BackgroundWorker完成运行事件

30

我的C#应用程序有几个后台工作线程。有时,一个后台工作者会触发另一个后台工作者。当第一个后台工作者完成并触发RunWorkerCompleted事件时,该事件会在哪个线程上触发,UI线程还是调用RunWorkerAsync的第一个后台工作者线程?我正在使用Microsoft Visual C# 2008 Express Edition。如果您有任何想法或建议,请告诉我。谢谢。

3个回答

66
如果BackgroundWorker是从UI线程创建的,则RunWorkerCompleted事件也将在UI线程上引发。
如果它是从后台线程创建的,则该事件将在未定义的后台线程上引发(不一定是同一个线程,除非您正在使用自定义SynchronizationContext)。
有趣的是,这似乎并没有在MSDN上得到很好的记录。我能找到的最好的参考资料是这里
在您的应用程序中实现多线程的首选方法是使用BackgroundWorker组件。BackgroundWorker组件使用事件驱动模型进行多线程操作。后台线程运行您的DoWork事件处理程序,创建控件的线程运行您的ProgressChangedRunWorkerCompleted事件处理程序。 您可以从ProgressChangedRunWorkerCompleted事件处理程序调用您的控件。

9
这不正确。只有在UI线程创建了BGW实例时,它才会在UI线程上升起。如果一个线程创建了BGW,则它将在任意线程池线程上升起。 - Hans Passant
4
如果对象不是在UI主线程创建的,它就不会在同一线程中引发。调用任意线程的Marshaling操作是不可能的,只有UI主线程具备所需的基础设施。需要使用WindowsFormsSynchronizationContext或DispatcherSynchronizationContext提供程序,因为缺省提供程序(SynchronizationContext)会在线程池线程上进行回调。 - Hans Passant
1
@Hans:好的,我已经让这个更清晰了。只要我们挑剔一点,它不必是UI线程;你可以创建自己的“同步上下文”,它确实可以同步。 - Aaronaught
2
我们感到困惑的原因是您还必须从主线程调用RunWorkerAsync。这是最重要的。我们有一个计时器来收集BGWs并启动它们。System.Timers.Timer和System.Windows.Forms.Timer之间的区别在于OnWorkerCompleted是从主线程还是随机线程调用的。System.Timers.Timer事件不会在主线程上调用。 - Thadeux
1
@TobiasKnauss:不,当前版本是正确的。回调在同一个“同步上下文”中运行,而不是同一个“线程”中运行。如果从随机的“线程池”线程调用RunWorkerAsync,我几乎可以保证回调不会在同一个线程上运行。 - Aaronaught
显示剩余4条评论

0
根据我的观察,RunWorkerCompleted 方法是在调用 RunWorkerAsync 的线程上执行的,而不一定是创建后台工作者的线程。

-1

在同一个问题上,我找到了这个线程msdn

这都与SynchronizationContext有关。
当Windows Forms创建一个窗口时,默认情况下会自动覆盖任何现有的SynchronizationContext;请参阅WindowsFormsSynchronizationContext.AutoInstall: http://msdn.microsoft.com/en-us/library/system.windows.forms.windowsformssynchronizationcontext.autoinstall.aspx BackgroundWorker在调用RunWorkerAsync时捕获SynchronizationContext,而不是在构造时(请注意,这是一个实现细节,未记录)。然后,它使用捕获的SynchronizationContext来执行RunWorkerCompleted。
因此,运行RunWokerCompleted的线程实际上是由RunWorkerAsync调用时的SynchronizationContext.Current确定的。
WPF提供了一个DispatcherSynchronizationContext,它将调用传递到其UI线程。Windows Forms提供了一个WindowsFormsSynchronizationContext,它将调用传递到其UI线程。在进行WPF/Forms互操作时,两个系统共享单个UI线程。您提到这都是在MFC应用程序的上下文中;在这种情况下,MFC不提供SynchronizationContext(当然),但它将与WPF和Forms线程共享其线程(它们都共享单个UI线程)。
还有一条信息:默认的SynchronizationContext将操作(例如,RunWorkerCompleted)排队到ThreadPool。这听起来就像您看到的行为。如果在调用RunWorkerAsync时SynchronizationContext.Current为空,则会出现此默认行为。
因此,关闭Windows Forms表单似乎会清除SynchronizationContext.Current。Windows Forms确实有一个“主表单”的概念,因此它可能会这样做,因为它认为应用程序的最后一个表单刚刚关闭。
我建议:1)测试在调用RunWorkerCompleted时SynchronizationContext.Current是否为空,并在显示Windows Forms表单之前和之后进行测试。只是为了确保这是问题所在。2)在Windows Forms表单关闭后设置SynchronizationContext.Current(通过SynchronizationContext.SetSynchronizationContext)。将“new DispatcherSynchronizationContext()”作为参数传递。
或者,如果Windows Forms表单是模态对话框,则可以使用我的Nito.Async库中的ScopedSynchronizationContext类(http://www.codeplex.com/NitoAsync),它是一个非常简单的类,用于临时替换SynchronizationContext.Current,并在其“using”块结束时将其重置为其原始值。
有关各种SynchronizationContext类型的更多信息,请参阅我的博客: http://nitoprograms.blogspot.com/2009/10/synchronizationcontext-properties.html

简而言之,插入

AsyncOperationManager.SynchronizationContext = new DispatcherSynchronizationContext(this.Dispatcher)

在调用RunWorkerAsync之前。


1
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心找到有关如何编写良好答案的更多信息。 - Community

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