后台工作程序:确保在执行RunWorkerCompleted之前已完成ProgressChanged方法

我们假设我正在使用后台工作器,我有以下方法:

private void bw_DoWork(object sender, DoWorkEventArgs e) { finalData = MyWork(sender as BackgroundWorker, e); } private void bw_ProgressChanged(object sender, ProgressChangedEventArgs e) { int i = e.ProgressPercentage; // Missused for i Debug.Print("BW Progress Changed Begin, i: " + i + ", ThreadId: " + Thread.CurrentThread.ManagedThreadId); // I use this to update a table and an XY-Plot, so that the user can see the progess. UpdateGUI(e.UserState as MyData); Debug.Print("BW Progress Changed End, i: " + i + ", ThreadId: " + Thread.CurrentThread.ManagedThreadId); } private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if ((e.Cancelled == true)) { // Cancelled } else if (!(e.Error == null)) { MessageBox.Show(e.Error.Message); } else { Debug.Print("BW Run Worker Completed Begin, ThreadId: " + Thread.CurrentThread.ManagedThreadId); // I use this to update a table and an XY-Plot, // so that the user can see the final data. UpdateGUI(finalData); Debug.Print("BW Run Worker Completed End, ThreadId: " + Thread.CurrentThread.ManagedThreadId); } } 

现在我假设bw_RunWorkerCompleted方法在调用bw_RunWorkerCompleted方法之前已经完成。 但事实并非如此,我不明白为什么?

我得到以下输出:

 Worker, i: 0, ThreadId: 27 BW Progress Changed Begin, i: 0, ThreadId: 8 BW Progress Changed End, i: 0, ThreadId: 8 Worker, i: 1, ThreadId: 27 BW Progress Changed Begin, i: 1, ThreadId: 8 BW Progress Changed End, i: 1, ThreadId: 8 Worker, i: 2, ThreadId: 27 BW Progress Changed Begin, i: 2, ThreadId: 8 BW Run Worker Completed Begin, ThreadId: 8 BW Run Worker Completed End, ThreadId: 8 A first chance exception of type 'System.InvalidOperationException' occurred in mscorlib.dll ERROR <-- Collection was modified; enumeration operation may not execute. ERROR <-- NationalInstruments.UI.WindowsForms.Graph.ClearData() 

MagagedID 8是Main Thread ,27是Worker Thread 。 我可以在Debug / Windows / Threads中看到这个。

如果我没有在bw_ProgressChanged方法中调用UpdateGUI int,则不会发生错误。 但随后用户在表格和XY图中看不到任何进展。

编辑

MyWork方法看起来像这样:

 public MyData[] MyWork(BackgroundWorker worker, DoWorkEventArgs e) { MyData[] d = new MyData[n]; for (int i = 0; i < n; i++) d[i] = null; for (int i = 0; i < n; i++) { if (worker.CancellationPending == true) { e.Cancel = true; break; } else { d[i] = MyCollectDataPoint(); // takes about 1 to 10 seconds Debug.Print("Worker, i: " + i + ", ThreadId: " + Thread.CurrentThread.ManagedThreadId) worker.ReportProgress(i, d); } } return d; } 

并且UpdateGUI方法看起来像这样:

 private void UpdateGUI(MyData d) { UpdateTable(d); // updates a DataGridView UpdateGraph(d); // updates a ScatterGraph (NI Measurement Studio 2015) } 

如果我不调用UpdateGraph方法,它就像预期的那样工作。 因此,在执行RunWorkerCompleted之前, ProgressChanged方法已完成。

所以我想问题是NI Measurement Studio 2015的ScatterGraphBackgroundWorker 。 但我不明白为什么?

UpdateGraph方法如下所示:

 private void UpdateGraph(MyData d) { plot.ClearData(); plot.Plots.Clear(); // The error happens here (Collection was modified; enumeration operation may not execute). int n = MyGetNFromData(d); for (int i = 0; i < n; i++) { ScatterPlot s = new ScatterPlot(); double[] xi = MyGetXiFromData(d, i); double[] yi = MyGetYiFromData(d, i); s.XAxis = plot.XAxes[0]; s.YAxis = plot.YAxes[0]; s.LineWidth = 2; s.LineColor = Colors[i % Colors.Length]; s.ProcessSpecialValues = true; s.PlotXY(xi, yi); plot.Plots.Add(s); } } 

编辑2

如果我在bw_RunWorkerCompleted方法中设置断点,则调用堆栈如下所示:

 bw_RunWorkerCompleted [External Code] UpdateGraph // Line: plot.ClearData() UpdateGUI bw_ProgressChanged [External Code] Program.Main 

和第一个[External Code]块:

 System.dll!System.ComponentModel.BackgroundWorker.OnRunWorkerCompleted(System.ComponentModel.RunWorkerCompletedEventArgs e) Unknown [Native to Managed Transition] mscorlib.dll!System.Delegate.DynamicInvokeImpl(object[] args) Unknown System.Windows.Forms.dll!System.Windows.Forms.Control.InvokeMarshaledCallbackDo(System.Windows.Forms.Control.ThreadMethodEntry tme) Unknown System.Windows.Forms.dll!System.Windows.Forms.Control.InvokeMarshaledCallbackHelper(object obj) Unknown mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Unknown mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Unknown mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state) Unknown System.Windows.Forms.dll!System.Windows.Forms.Control.InvokeMarshaledCallback(System.Windows.Forms.Control.ThreadMethodEntry tme) Unknown System.Windows.Forms.dll!System.Windows.Forms.Control.InvokeMarshaledCallbacks() Unknown System.Windows.Forms.dll!System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control caller, System.Delegate method, object[] args, bool synchronous) Unknown System.Windows.Forms.dll!System.Windows.Forms.Control.Invoke(System.Delegate method, object[] args) Unknown System.Windows.Forms.dll!System.Windows.Forms.WindowsFormsSynchronizationContext.Send(System.Threading.SendOrPostCallback d, object state) Unknown NationalInstruments.Common.dll!NationalInstruments.Restricted.CallbackManager.CallbackDispatcher.SynchronousCallbackDispatcher.InvokeWithContext(System.Delegate handler, object sender, System.EventArgs e, System.Threading.SynchronizationContext context, object state) Unknown NationalInstruments.Common.dll!NationalInstruments.Restricted.CallbackManager.a(NationalInstruments.Restricted.CallbackManager.CallbackDispatcher A_0, object A_1, object A_2, System.EventArgs A_3) Unknown NationalInstruments.Common.dll!NationalInstruments.Restricted.CallbackManager.RaiseEvent(object eventKey, object sender, System.EventArgs e) Unknown NationalInstruments.Common.dll!NationalInstruments.ComponentBase.RaiseEvent(object eventKey, System.EventArgs e) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.XYCursor.OnAfterMove(NationalInstruments.UI.AfterMoveXYCursorEventArgs e) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.XYCursor.a(object A_0, NationalInstruments.Restricted.ControlElementCursorMoveEventArgs A_1) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.XYCursorElement.OnAfterMove(NationalInstruments.Restricted.ControlElementCursorMoveEventArgs e) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.XYCursorElement.a(NationalInstruments.UI.Internal.CartesianPlotElement A_0, double A_1, double A_2, int A_3, bool A_4) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.XYCursorElement.MoveCursorFreely(double xValue, double yValue, bool isInteractive, NationalInstruments.UI.Internal.XYCursorElement.Movement movement) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.XYCursorElement.MoveCursorXY(double xValue, double yValue, bool isInteractive) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.XYCursorElement.ResetCursor() Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.XYCursorElement.a(object A_0, NationalInstruments.Restricted.ControlElementEventArgs A_1) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.PlotElement.OnDataChanged(NationalInstruments.Restricted.ControlElementEventArgs e) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.PlotElement.OnDataChanged() Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.CartesianPlotElement.a(object A_0, NationalInstruments.UI.Internal.PlotDataChangedEventArgs A_1) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.XYDataManager.a(NationalInstruments.UI.Internal.PlotDataChangedEventArgs A_0) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.XYDataManager.a(NationalInstruments.UI.Internal.PlotDataChangeCause A_0, int A_1) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.XYDataManager.ClearData(bool raiseDataChanged) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.CartesianPlotElement.ClearData(bool raiseDataChanged) Unknown NationalInstruments.UI.dll!NationalInstruments.UI.Internal.PlotElement.ClearData() Unknown NationalInstruments.UI.dll!NationalInstruments.Restricted.XYGraphManager.ClearData() Unknown NationalInstruments.UI.WindowsForms.dll!NationalInstruments.UI.WindowsForms.Graph.ClearData() Unknown 

好吧,您有确凿的证据表明在ProgressChanged事件运行 RunWorkerCompleted事件会运行。 当然,这通常是不可能的,它们应该在同一个线程上运行。

无论如何,有两种可能的方式。 更明显的一点是事件处理程序实际上并不在UI线程上运行。 这是相当常见的事故,尽管您倾向于注意到导致的InvalidOperationException。 然而,该exception并不总是可靠地提出,它使用启发式方法。 请注意,您的UpdateGraph()方法不太可能使其失效,因为它似乎不使用标准的.NET控件。

诊断此事故很简单,只需在事件处理程序上设置断点并使用Debug> Windows> Threads调试窗口validation它是否在主线程上运行。 使用Debug.Print显示Thread.CurrentThread.ManagedId的值可以帮助确保所有调用都在UI线程上运行。 您可以通过确保在主线程上执行RunWorkerAsync()调用来修复它。

然后有一个重新入侵错误的陷阱,它发生在ProgressChanged做了一些让UI调度程序再次运行的东西时。 趋向于像线程竞赛一样难以调试。 可能发生的三种基本方式:

  • 使用臭名昭着的Application.DoEvents()

  • 它的邪恶的继姐妹,ShowDialog()。 ShowDialog是伪装的DoEvents,它假装通过禁用UI的窗口来降低致命性。 除非您运行未由UI激活的代码,否则它往往可以正常工作。 喜欢这段代码。 请注意,您似乎确实使用MesssageBox.Show()进行调试,从来不是一个好主意。 始终支持断点和Debug.Print()以避免此陷阱。

  • 做一些阻止UI线程的事情,比如lock,Thread.Join(),WaitOne()。 阻止STA线程正式违法,死锁的可能性很高,所以CLR对此做了些什么。 它泵送自己的消息循环以确保避免死锁。 是的,就像DoEvents一样,它会进行一些过滤以避免令人讨厌的情况。 但对于这段代码来说还不够。 请注意,这可能是由您未编写的代码完成的,例如Graph控件。

通过在RunWorkerCompleted事件上设置断点来诊断重新入侵错误。 您应该看到ProgressChanged事件处理程序返回,深埋在调用堆栈中。 以及导致重新进入的声明。 如果跟踪无法帮助您解决问题,请将其发布在您的问题中。

最大的缺陷是你的假设下面是错误的。

现在我假设bw_ProgressChanged方法在调用bw_RunWorkerCompleted方法之前已经完成。 但事实并非如此,我不明白为什么?

不要陷入精神上的序列化逻辑流程。 使用WinForms / WPF,您会发生两个完全独立且异步的事件。 您让BGW向UI发送请求(通过worker.ReportProgress )以执行进度更新。 UI线程必须在bw_ProgressChanged事件运行时接收该请求并安排。

独立于BGW(通过myWork )决定终止,可能是通过完全完成作业,或者因为抛出未捕获的exception,或者最终用户可能希望取消给定实例的工作。 然后,它向UI线程发送请求以运行bw_RunWorkerCompleted方法。 UI必须再次将其安排在许多要做的事情列表中。