如何在多个线程之间进行通信?

11

我正在为另一个程序编写插件,该程序使用本地程序打开一系列文件来提取一些数据。我遇到的一个问题是这个进程需要很长时间,而我希望保持用户界面不会挂起。此外,我还想在进程完成之前让用户有能力取消进程。以前我使用了一个后台工作器来处理这种情况,但在这种情况下,我认为BackgroundWorker不适用。

通过我正在使用的API创建插件,可以通过继承IAPICommand接口来创建自定义命令。该接口包括一个Execute(Application app)方法。然后实例化该类,并在用户在程序中调用自定义命令时由程序调用Execute()方法。

当调用Execute()方法时,传递了对当前Application对象的引用,正是这个应用程序对象用于打开要从中提取数据的文件。然而,应用程序实例无法在除原始Execute()线程之外的其他线程请求时打开文档。

因此,通常UI将存在于主线程上,而耗时的数据提取将在辅助线程上执行。但在这种情况下,必须在主线程上执行数据提取,并且我需要为UI创建辅助线程。

以下是代码的简化版本。

class MyCommand:IAPICommand
{
    public void Execute(Application app) // method from IAPICommand
    {
        Thread threadTwo= new Thread(ShowFormMethod);
        threadTwo.Start();
    }

    public void ProcessWidget(Widget w, Application app)
    { 
        //uses an App to work some magic on C
        //app must be called from the original thread that called ExecuteCommand()
    }

    //method to open custom form on a seperatethread
    public void ShowFormMethod()
    {
      MyForm form = new MyForm();
      form.ShowDialog();  
    }
}

这是一个流程图,展示了我认为这个问题应该如何解决。

alt text http://dl.dropbox.com/u/113068/SOMLibThreadingDiagram.jpg

  1. 这个图表有意义吗?如果有的话,我是否采取了正确的方法来解决这个问题?
  2. 一旦主线程启动UI线程,我希望它等待用户选择要处理的小部件或通过关闭窗体(图表中的红色数字)来结束命令。我如何让主线程等待,并且如何触发它继续进行处理或在UI线程结束时继续到最后?我想我可以让主线程等待一个监视器锁。然后UI线程会填充要处理的小部件的静态列表,然后脉冲主线程以触发处理。当窗体关闭时,UI线程还会脉冲主线程,如果要处理的小部件列表为空时脉冲主线程,那么主线程将知道继续到命令的最后。
  3. 如何允许主线程向UI线程传递小部件处理的进度或完成情况(图表中的黄色箭头)?我只需要使用Form的BeginInvoke()方法来实现吗?
  4. 如何允许UI线程取消小部件处理(图表中的绿色箭头)?我想我可以设置一个静态布尔标志,在处理每个小部件之前检查它是否被取消了?

正如John所指出的那样,如果您在主UI线程中完成所有UI相关的工作并将繁重的处理任务委托给后台线程,那么您的生活将变得轻松得多。在这种情况下,通过Control.Invoke方法进行通信非常容易。 - mfeingold
如果您必须使用的线程正在运行处理并且不可用于回调到应用程序,那么如何取消处理呢?您可以在第二个线程上取消,但不能在第二个线程上取消处理。 - John Knoeller
是的,用户将在第二个(UI)线程上请求取消,以取消主应用程序线程上的处理。 - Eric Anastas
好漂亮的图表,可以问一下你用的是哪个工具制作的吗? - RBarryYoung
4个回答

14

通常在应用程序中有多个线程创建表单是不明智的做法。虽然这不是不可能实现的,但比你想象的要困难得多,因为处于父子关系的表单会向彼此发送消息,并且当它们这样做时,发送消息的那个将被阻塞,直到接收方处理完毕。

如果加入了显式的线程间消息传递或同步,很容易出现死锁。因此,通常最好确保主线程保留给用户界面,并在其他没有 UI 的线程中进行所有处理。

如果你遵循这种设计,后台线程可以使用 Control.BeginInvoke 传递消息到 UI 线程,而无需等待消息被处理。


我理解你的意思,但由于我的程序是另一个程序的插件,我并没有真正控制“处理”所在的线程。它必须在插件的线程中完成,因此为了将UI与处理分离,UI必须放在辅助线程中。 - Eric Anastas
@eric:那就很糟糕了。Control.BeginInvoke仍然可以让你从后台线程到达UI线程,而不需要等待,你能够避免后台线程等待UI线程的时间越长,你就会越好。如果其中一个线程从未等待,就不会出现死锁问题。 - John Knoeller
哦,也许你误解了我的问题。我没有在多个线程中创建表单或任何UI元素。我只有一个窗体,在第二个线程中创建,因为我需要第一个“主”线程来进行处理,因为它是唯一能够访问父级应用程序的线程。 - Eric Anastas
我理解,有两个线程,都有表单;应用程序线程和您的表单线程。您正在使用应用程序线程作为处理线程(对于您来说,它是一个“后台”线程,因为您的UI在另一个线程上)。这是最糟糕的设计,但我相信您知道自己的情况并且没有选择,所以很遗憾... - John Knoeller
顺便说一下,这对于插件供应商来说并不是一个不寻常的情况。许多VST(音频)插件供应商都有这个确切的问题(在C++中,而不是C#,但仍然存在相同的线程问题)。 - John Knoeller

2

除了其他答案之外,我建议您使用从ProcessWidget回调方法将进度传递回调用线程。如果更新了调用者后,您可以使用回调来向工作线程返回停止信号以提前停止它。或者使用单独的回调方法定期检查是否可以继续执行。或者设置(喘气!)全局静态标志,工作线程定期检查。或在工作线程上调用Thread.Abort并使其捕获ThreadAbortException以清理任何资源。


1

我假设宿主应用程序是WinForms应用程序。

您需要在Execute方法中保存原始线程的SynchronizationContext,然后调用其Send方法在宿主的UI线程上执行代码。

例如:

class MyCommand:IAPICommand
{
    SynchronzationContext hostContext;
    public void Execute(Application app) // method from IAPICommand
    {
        hostContext = SynchronzationContext.Current;
        Thread threadTwo = new Thread(ShowFormMethod);
        threadTwo.Start();
    }

    public void ProcessWidget(Widget w, Application app)
    { 
        //uses an App to work some magic on C
        //app must be called from the original thread that called ExecuteCommand()
        SomeType someData = null;
        hostContext.Send(delegate { someData = app.SomeMethod(); }, null);
    }
}

啊,好的,但是表单如何到达“ProcessWidget()”或“hostContext”呢?当我在另一个线程上创建表单时,难道不需要以某种方式传递对其中一个的引用吗? - Eric Anastas
是的,您可以将 MyCommand 类的实例传递给表单。 - SLaks
好的,我开始搞清楚了。我如何让原始线程等待辅助窗体线程?实际上,Execute()返回一个枚举值,表示命令是否成功。我如何让主要的Execute()线程等待来自FormThread的请求,然后重新路由到ProcessWidget()方法?在用户完成Form线程之前,我不希望Execute()线程返回到主程序。 - Eric Anastas
好的,我正在尝试按照您描述的使用SynchronizationContext来使其工作。我在我的hostContext.Send()命令处设置了断点,并在SomeMethod()的顶部设置了断点。程序到达对hostContext.Send()的调用,但一旦调用,程序似乎就停止了。它从未触发SomeMethod()中的断点,也没有触发hostContext.Send()行后的断点。 - Eric Anastas

0

如果你看一下Java Swing,它是如何做到这一点的一个很好的例子:

1)一个主线程负责处理所有UI请求。这消除了应用程序中的任何竞争条件。

2)每当需要执行任何“工作”时,就会生成一个线程(或线程池)并执行该工作。因此,主线程除了几微秒外不会被阻塞,而UI在进行任何操作时都是完全响应的。

3)在所有语言中都必须有一个线程中断机制。在Java中,您可以在线程上调用.interrupt(),当前运行的线程将在其执行的任何地方抛出InterruptedException。您的工作是捕获该异常,确定是否真正中断(请阅读javadoc以获取此部分),如果只是让自己死亡(从run方法返回)。

1 + 2 = 不显眼的客户端交互

3 = 终止线程

3的替代方法(如果3太复杂)是给线程一个方法.kill();该方法设置一个kill标志。当您正在从硬盘读取缓冲区时进行循环检查,如果设置了kill标志,则退出循环,关闭处理程序,并从run方法返回。

编辑:抱歉忘记提及进度报告:

你的线程应该有一个公开的线程安全方法来获取“进度报告”,或者说包含有关进度信息的数据结构。你的UI线程应该定期(比如每0.5秒)检查线程的进度报告并更新UI的进度条。通过UI线程检查,我的意思是显示进度的小部件会在计时器上请求重新渲染以获取最新信息,直到完成。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接