Как остановить BackgroundWorker в событии закрытия формы?

У меня есть форма, которая порождает BackgroundWorker, которая должна обновлять форму собственного текстового поля (в основном потоке), следовательно Invoke((Action) (...)); вызов.
Если в HandleClosingEvent я просто делаю bgWorker.CancelAsync(), тогда я получаю вызов ObjectDisposedException on Invoke(...), понятно. Но если я сижу в HandleClosingEvent и жду, когда bgWorker будет выполнен, то .Invoke(...) никогда не вернется, также понятно.

Любые идеи, как закрыть это приложение, не получая исключение, или тупик?

Ниже приведены 3 релевантных метода простого класса Form1:

    public Form1() {
        InitializeComponent();
        Closing += HandleClosingEvent;
        this.bgWorker.RunWorkerAsync();
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
        while (!this.bgWorker.CancellationPending) {
            Invoke((Action) (() => { this.textBox1.Text = Environment.TickCount.ToString(); }));
        }
    }

    private void HandleClosingEvent(object sender, CancelEventArgs e) {
        this.bgWorker.CancelAsync();
        /////// while (this.bgWorker.CancellationPending) {} // deadlock
    }

Ответ 1

Единственный безопасный и безопасный для безопасности способ, который я знаю, - фактически отменить событие FormClosing. Установите e.Cancel = true, если BGW все еще работает, и установите флаг, чтобы указать, что пользователь запросил закрыть. Затем проверьте этот флаг в обработчике событий BGW RunWorkerCompleted и вызовите Close(), если он установлен.

private bool closePending;

protected override void OnFormClosing(FormClosingEventArgs e) {
    if (backgroundWorker1.IsBusy) {
        closePending = true;
        backgroundWorker1.CancelAsync();
        e.Cancel = true;
        this.Enabled = false;   // or this.Hide()
        return;
    }
    base.OnFormClosing(e);
}

void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
    if (closePending) this.Close();
    closePending = false;
    // etc...
}

Ответ 2

Я нашел другой путь. Если у вас есть больше фоновых работников, вы можете сделать:

List<Thread> bgWorkersThreads  = new List<Thread>();

и в каждом случаеWowerWorker метод make:

bgWorkesThreads.Add(Thread.CurrentThread);

Артер, который вы можете использовать:

foreach (Thread thread in this.bgWorkersThreads) 
{
     thread.Abort();    
}

Я использовал это в надстройке Word в элементе управления, которое я использую в CustomTaskPane. Если кто-то закроет документ или приложение раньше, то все мои фоновые работы завершат их работу, он вызывает некоторые COM Exception (я не помню, что именно). CancelAsync() не работает.

Но при этом я могу закрыть все потоки, которые используются backgroundworkers Сразу в событии DocumentBeforeClose, и моя проблема решена.

Ответ 3

Вот мое решение (Извините, что в VB.Net).

Когда я запускаю событие FormClosing, я запускаю BackgroundWorker1.CancelAsync(), чтобы установить для параметра CancellationPending значение True. К сожалению, у программы никогда не будет возможности проверить значение CancellationPending, чтобы установить e.Cancel в true (что, насколько я могу судить, может быть сделано только в BackgroundWorker1_DoWork). Я не удалял эту строку, хотя на самом деле это не имеет никакого значения.

Я добавил строку, которая установила бы мою глобальную переменную bClosingForm в значение True. Затем я добавил строку кода в свой BackgroundWorker_WorkCompleted, чтобы проверить как e.Cancelled, так и глобальную переменную bClosingForm перед выполнением каких-либо завершающих шагов.

Используя этот шаблон, вы сможете закрыть свою форму в любое время, даже если фоновый работник находится в середине чего-то (что может быть не очень хорошо, но оно обязательно должно произойти, чтобы оно могло быть рассмотрено), Я не уверен, что это необходимо, но вы можете полностью избавить работника Background от события Form_Closed после этого.

Private bClosingForm As Boolean = False

Private Sub SomeFormName_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
    bClosingForm = True
    BackgroundWorker1.CancelAsync() 
End Sub

Private Sub backgroundWorker1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
    'Run background tasks:
    If BackgroundWorker1.CancellationPending Then
        e.Cancel = True
    Else
        'Background work here
    End If
End Sub

Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
    If Not bClosingForm Then
        If Not e.Cancelled Then
            'Completion Work here
        End If
    End If
End Sub

Ответ 4

Не можете ли вы ждать сигнала в деструкторе формы?

AutoResetEvent workerDone = new AutoResetEvent();

private void HandleClosingEvent(object sender, CancelEventArgs e)
{
    this.bgWorker.CancelAsync();
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    while (!this.bgWorker.CancellationPending) {
        Invoke((Action) (() => { this.textBox1.Text =   
                                 Environment.TickCount.ToString(); }));
    }
}


private ~Form1()
{
    workerDone.WaitOne();
}


void backgroundWorker1_RunWorkerCompleted( Object sender, RunWorkerCompletedEventArgs e )
{
    workerDone.Set();
}

Ответ 5

Во-первых, исключение ObjectDisposedException - это только одна возможная ошибка. Запуск кода OP вызвал следующее InvalidOperationException в значительном количестве случаев:

Invoke или BeginInvoke не может быть вызван на элементе управления до тех пор, пока ручка окна был создан.

Я предполагаю, что это можно было бы изменить, начав рабочий в обратном вызове "Loaded", а не в конструкторе, но все это испытание можно вообще избежать, если используется механизм отчетности BackgroundWorker Progress. Хорошо работает следующее:

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    while (!this.bgWorker.CancellationPending)
    {
        this.bgWorker.ReportProgress(Environment.TickCount);
        Thread.Sleep(1);
    }
}

private void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    this.textBox1.Text = e.ProgressPercentage.ToString();
}

Я вроде угнал процентный параметр, но можно использовать другую перегрузку для передачи любого параметра.

Интересно отметить, что удаление вышеупомянутого вызова сна забивает пользовательский интерфейс, потребляет высокий процессор и постоянно увеличивает использование памяти. Я предполагаю, что это связано с тем, что очередь сообщений перегружена графическим интерфейсом. Однако, когда вызов сна не поврежден, использование ЦП фактически равно 0, и использование памяти кажется прекрасным. Чтобы быть разумным, возможно, следует использовать более высокое значение, чем 1 мс? Экспертное мнение здесь будет оценено... Обновление. Похоже, что пока обновление не слишком частое, оно должно быть в порядке: Ссылка

В любом случае я не могу предвидеть сценарий, когда обновление GUI должно быть в пределах интервалов, не превышающих пару миллисекунд (по крайней мере, в сценариях, где человек смотрит графический интерфейс), поэтому я думаю, что большинство того, что отчет о времени будет правильным выбором

Ответ 6

Ваш фоновый работник не должен использовать Invoke для обновления текстового поля. Он должен попросить поток пользовательского интерфейса красиво обновить текстовое поле, используя событие ProgressChanged со значением, помещенным в прикрепленное текстовое поле.

Во время события Закрытое (или, возможно, событие Закрытие), поток пользовательского интерфейса запоминает, что форма закрыта до того, как она отменяет фонового исполнителя.

После получения progressChanged поток пользовательского интерфейса проверяет, закрыта ли форма, и только если нет, она обновляет текстовое поле.

Ответ 7

Это не сработает для всех, но если вы периодически делаете что-то в BackgroundWorker, как и каждую секунду или каждые 10 секунд (возможно, опрос сервера), это, похоже, хорошо работает, чтобы остановить процесс упорядоченным образом и без сообщений об ошибках (по крайней мере до сих пор) и легко следовать;

 public void StopPoll()
        {
            MyBackgroundWorker.CancelAsync(); //Cancel background worker
            AutoResetEvent1.Set(); //Release delay so cancellation occurs soon
        }

 private void bw_DoWork(object sender, DoWorkEventArgs e)
        {
            while (!MyBackgroundWorker.CancellationPending)
            {
            //Do some background stuff
            MyBackgroundWorker.ReportProgress(0, (object)SomeData);
            AutoResetEvent1.WaitOne(10000);
            }
    }

Ответ 8

Я действительно не понимаю, почему DoEvents считается таким плохим выбором в этом случае, если вы используете this.enabled = false. Я думаю, это сделало бы его довольно аккуратным.

protected override void OnFormClosing(FormClosingEventArgs e) {

    this.Enabled = false;   // or this.Hide()
    e.Cancel = true;
    backgroundWorker1.CancelAsync();  

    while (backgroundWorker1.IsBusy) {

        Application.DoEvents();

    }

    e.cancel = false;
    base.OnFormClosing(e);

}

Ответ 9

Я передал бы SynchronizationContext, связанный с текстовым полем, в BackgroundWorker и использовал его для выполнения обновлений в потоке пользовательского интерфейса. Используя SynchronizationContext.Post, вы можете проверить, установлен ли элемент управления или утилизация.

Ответ 10

Как насчет Me.IsHandleCreated?

    Private Sub BwDownload_RunWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BwDownload.RunWorkerCompleted
    If Me.IsHandleCreated Then
        'Form is still open, so proceed
    End If
End Sub

Ответ 11

Одно решение, которое работает, но слишком сложное. Идея состоит в том, чтобы создать таймер, который будет продолжать пытаться закрыть форму, и форма не будет закрыта, пока не будет мертв bgWorker.

private void HandleClosingEvent(object sender, CancelEventArgs e) {
    if (!this.bgWorker.IsBusy) {
        // bgWorker is dead, let Closing event proceed.
        e.Cancel = false;
        return;
    }
    if (!this.bgWorker.CancellationPending) {
        // it is first call to Closing, cancel the bgWorker.
        this.bgWorker.CancelAsync();
        this.timer1.Enabled = true;
    }
    // either this is first attempt to close the form, or bgWorker isn't dead.
    e.Cancel = true;
}

private void timer1_Tick(object sender, EventArgs e) {
    Trace.WriteLine("Trying to close...");
    Close();
}

Ответ 12

Другой способ:

if (backgroundWorker.IsBusy)
{
    backgroundWorker.CancelAsync();
    while (backgroundWorker.IsBusy)
    {
        Application.DoEvents();
    }
}