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

Wol*_*ich 3 c# backgroundworker race-condition winforms measurement-studio

让我们假设我正在使用后台工作人员并且我有以下方法:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我假设该bw_ProgressChanged方法在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()
Run Code Online (Sandbox Code Playgroud)

MagagedID 8 是Main Thread, 27 是Worker Thread。我可以在调试/Windows/线程中看到这一点。

如果我不调用UpdateGUIintbw_ProgressChanged方法,则不会发生错误。但是随后用户在表格和 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;
}
Run Code Online (Sandbox Code Playgroud)

UpdateGUI方法看起来像这样:

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

如果我不调用UpdateGraph方法,它会按方面工作。所以该ProgressChanged方法在执行之前已经完成RunWorkerCompleted

所以我猜问题是ScatterGraph来自 NI Measurement Studio 2015 和BackgroundWorker. 但我不明白为什么?

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑 2

如果我在bw_RunWorkerCompleted方法中设置了一个断点,那么调用堆栈看起来像这样:

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

和第一个[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
Run Code Online (Sandbox Code Playgroud)

Han*_*ant 5

那么,你有确凿证据证明该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 事件处理程序返回,它深埋在调用堆栈中。以及导致重入的语句。如果跟踪不能帮助您弄清楚,请将其发布在您的问题中。

  • 这确实是一个重入错误,由 National Instruments 控制引起。它相当于 Control.Invoke() 来引发事件。它非常努力地确保在 UI 线程上引发事件。不必要地如此,并且对于 UI 控件毫无意义,代码已经在 UI 线程上运行。您需要向公司提交错误,但他们不太可能修复它。对于过去犯过此错误的客户,这是一种解决方法,即从工作线程更新控件。除了避免在 ProgressChanged 中更新图表之外,您没有其他解决方法。 (3认同)
  • 也许您需要像以前的客户一样错误地执行此操作,更新 DoWork 内部的图形而不是 ProgressChanged。我不知道可能触发多少种族错误。它们的频率要低得多,而且更难诊断。最好与 NI 程序员交谈。 (2认同)