Фоновый рабочий: убедитесь, что метод ProgressChanged завершен, прежде чем выполнять RunWorkerCompleted

Давайте предположим, что я использую Background Worker и у меня есть следующие методы:

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_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()

MagagedID 8 является Main Thread и 27 является Worker Thread, Я могу видеть это в Debug / Windows / Threads.

Если я не позвоню UpdateGUI в bw_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;
}

и UpdateGUI метод выглядит так:

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

Если я не позвоню 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);
    }
}

Редактировать 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

2 ответа

Решение

Ну, у вас есть веские доказательства того, что событие RunWorkerCompleted выполняется во время выполнения события ProgressChanged. Конечно, это обычно невозможно, они должны работать в одном потоке.

В любом случае это может произойти двумя способами. Более очевидным является то, что обработчики событий на самом деле не работают в потоке пользовательского интерфейса. Что является довольно распространенным несчастным случаем, хотя вы склонны замечать из InvalidOperationException, что вызывает. Однако это исключение не всегда достоверно, оно использует эвристику. Помните, что ваш метод UpdateGraph() вряд ли отключит его, так как он не использует стандартный элемент управления.NET.

В противном случае диагностировать этот сбой просто, просто установите точку останова на обработчике событий и используйте окно отладки Debug > Windows > Threads, чтобы убедиться, что он выполняется в главном потоке. Использование Debug.Print для отображения значения Thread.CurrentThread.ManagedId может помочь обеспечить выполнение всех вызовов в потоке пользовательского интерфейса. Вы исправите это, убедившись, что вызов RunWorkerAsync() выполняется в главном потоке.

И затем есть крысиная ловушка ошибки повторного входа, это происходит, когда ProgressChanged делает что-то, что снова запускает диспетчер пользовательского интерфейса. Сложно отлаживать его, как многопоточную гонку. Три основных способа, которые могут произойти:

  • используя печально известный Application.DoEvents()

  • его злая сводная сестра, ShowDialog(). ShowDialog - замаскированный DoEvents, он притворяется менее смертоносным, отключая окна пользовательского интерфейса. Что, как правило, работает нормально, кроме случаев, когда вы запускаете код, который не активируется пользовательским интерфейсом. Понравился этот код Остерегайтесь того, что вы, похоже, используете MesssageBox.Show() для отладки, но это не очень хорошая идея. Всегда используйте точки останова и Debug.Print(), чтобы избежать этой ловушки.

  • делать что-то, что блокирует поток пользовательского интерфейса, например блокировку, Thread.Join(), WaitOne(). Блокировка потока STA формально недопустима, высокие шансы на тупик, поэтому CLR что-то с этим делает. Он прокачивает свой собственный цикл сообщений, чтобы избежать тупиковых ситуаций. Да, как DoEvents, он выполняет некоторую фильтрацию, чтобы избежать неприятных случаев. Но не достаточно для этого кода. Помните, что это может быть сделано с помощью кода, который вы не написали, например, этот элемент управления Graph.

Диагностируйте ошибку повторного входа, установив точку останова для события RunWorkerCompleted. Вы должны увидеть обработчик событий ProgressChanged обратно, глубоко закопанный в стек вызовов. И утверждение, которое вызывает повторный вход. Если трассировка не поможет вам разобраться, то напишите об этом в своем вопросе.

Самым большим недостатком является ваше предположение ниже неверно.

Теперь я предположил бы, что метод bw_ProgressChanged завершился до вызова метода bw_RunWorkerCompleted. Но это не так, и я не понимаю, почему?

Не увлекайтесь мысленно сериализацией потока логики. С WinForms/WPF у вас есть два совершенно независимых и асинхронных события. BGW отправляет запрос (через worker.ReportProgress) в пользовательский интерфейс, чтобы выполнить обновление прогресса. Поток пользовательского интерфейса должен получить этот запрос и запланировать, когда bw_ProgressChanged событие проходит.

Независимо от этого BGW (через myWork) решает прекратить работу, возможно, полностью завершив задание, или потому что возникло исключение без перехвата, или, возможно, конечный пользователь пожелал отменить работу в данном экземпляре. Затем он отправляет запрос в поток пользовательского интерфейса для запуска bw_RunWorkerCompleted метод. Еще раз пользовательский интерфейс должен запланировать это в своем большом списке вещей, которые нужно сделать.

Другие вопросы по тегам