后台工作器:确保 ProgressChanged 方法在执行 RunWorkerCompleted 之前已经完成。

4

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

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_ProgressChanged方法已经完成了。但事实并非如此,我不明白为什么。

我收到以下输出:

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是主线程,27是一个工作线程。我可以在Debug / Windows / Threads中看到这一点。
如果我在方法中不调用UpdateGUI,则不会发生任何错误。但是用户无法在表格和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方法,则结果符合预期。因此ProgressChanged方法已在执行RunWorkerCompleted之前完成。
所以我猜问题出在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

并且第一个[外部代码]块:

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

1
UpdateGUI 是否调用任何异步方法,其中您不等待任务完成? - René Vogt
这真的很奇怪...在你的输出的最后一行中,你添加了“未执行”的字样...只是为了澄清:这个结束消息从progresschanged发生在输出中还是完全缺失?后者意味着进度更改已经在输出之前返回,尽管我不知道这是如何发生的...除非你没有在其他地方捕获和隐藏它,否则异常应该会使应用程序崩溃。你是否尝试过调试(首先确定i的哪个值是最后一个进度更改事件)? - René Vogt
但是Rick有一点:你没有指定这是Windows Forms还是WPF。我不太确定WPF UI线程中消息泵的行为与WinForms消息泵有多大不同。我的假设完全基于WinForms。 - René Vogt
使用 if (i < n - 1) worker.ReportProgress(i, d); 可以避免错误,但这是一种非常愚蠢的解决方法。我不喜欢那样做。我想要理解它。 - Wollmich
我已经没有更多的想法了。你能否在progresschanged处理程序中添加try/catch块并检查是否有任何异常抛出?这真的是我的最后一个想法,尽管这可能会导致您的应用程序崩溃(只要您不通过Application.ThreadException处理程序全局捕获异常),而且它并不能真正解释后来的枚举异常。 - René Vogt
显示剩余7条评论
2个回答

6

你已经掌握了强有力的证据,表明RunWorkerCompleted事件在ProgressChanged事件运行时同时进行。当然这通常是不可能的,因为它们应该在同一个线程上运行。

这种情况发生的两种可能性。更明显的一种是事件处理程序实际上并未在UI线程上运行。虽然这种错误很常见,但通常会通过InvalidOperationException来引起注意。然而,这个异常并不总是可靠地被引发,它使用启发式方法。请注意,你的UpdateGraph()方法不太可能引发该异常,因为它似乎没有使用标准的.NET控件。

诊断此错误非常容易,只需在事件处理程序上设置断点,并使用Debug> Windows> Threads调试窗口验证它在主线程上运行。使用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事件处理程序返回并深入调用堆栈。还有引起重入的语句。如果跟踪无法帮助你找到解决方法,请将其发布在问题中。


如果是选项1,那么问题确实出现在该处理程序中的某个地方,但它可能会更深入地调用堆栈或任何它正在操作的控件的代码中。如果是选项2,则问题不在进度更改处理程序中。 - Servy
谢谢你的回答。如果我在 plot.Plots.Clear() 处设置断点,我可以看到这总是在 Main Thread 中。删除 MessageBox.Show("Done!") 也没有帮助。所以我猜这是一个 re-entrancy bug。但我还是不理解它,并且正在寻找一种解决方法。 - Wollmich
1
通过在RWC事件处理程序上设置断点来诊断可重入错误。您应该在调用堆栈的深处看到ProgressChanged事件处理程序。如果是这种情况,但仍然无法理解,请在问题中发布堆栈跟踪。还要Debug.Print Thread.CurrentThread.ManagedId,以便您可以确保每个调用都发生在正确的线程上。 - Hans Passant
3
这确实是一个重新进入的漏洞,由国家仪器控制引起。它在执行Control.Invoke()来触发事件。它过于努力确保事件在UI线程上触发。对于UI控件而言这是不必要的,因为该代码已经在UI线程上运行了。您需要向该公司提交一个缺陷报告,但他们修复的可能性相当小。这是为以前错误地从工作线程更新控件的客户提供的解决方法。您没有其他解决方法,除了避免在ProgressChanged中更新图表。 - Hans Passant
2
也许你需要像之前的客户一样做错,而是在DoWork内更新图表而不是ProgressChanged。我不知道这可能会引发多少竞争错误。它们发生的频率要少得多,而且更难诊断。最好与NI程序员交流。 - Hans Passant
显示剩余4条评论

-1

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

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

不要被逻辑流程所困扰。在 WinForms/WPF 中,您有两个完全独立和异步的事件发生。您有 BGW 发送请求(通过 worker.ReportProgress)到 UI 执行进度更新。UI 线程必须接收该请求并安排 bw_ProgressChanged 事件运行的时间。

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


1
但是这两个事件在工作线程中一个接一个地发生。如果 OP 能够使 UI 线程可重复地安排后面的事件先于前面的事件,我会非常惊讶。当然,myWork 不会等待 progresschanged 完成,但 progresschanged 明确地在 workercompleted 之前被安排。你甚至可以在输出中看到这一点(progresschanged 在 completed 之前开始)。而且这两个事件在同一个 UI 线程上执行,而不是并行执行。 - René Vogt
@RenéVogt 是的,progresschanged在workercompleted之前被调用,但这并不意味着progresschanged会在workercompleted被调用之前完成,这正是OP所问的。 - Rick Davin
1
是的,它确实可以(在“正常”情况下,而不是Hans所描述的情况下)。处理程序(通常)都在同一个UI线程上执行,因此只有在“progresschanged”完成后,“completed”才能运行。BackgroundWorker负责在UI线程上“调用”处理程序。 - René Vogt

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