Как подождать, когда BackgroundWorker отменит?

Рассмотрим гипотетический метод объекта, который делает вещи для вас:

public class DoesStuff
{
    BackgroundWorker _worker = new BackgroundWorker();

    ...

    public void CancelDoingStuff()
    {
        _worker.CancelAsync();

        //todo: Figure out a way to wait for BackgroundWorker to be cancelled.
    }
}

Как можно ждать выполнения BackgroundWorker?


В прошлом люди пытались:

while (_worker.IsBusy)
{
    Sleep(100);
}

Но это взаимоблокировки, потому что IsBusy не очищается до тех пор, пока не будет обработано событие RunWorkerCompleted, и это событие не сможет получить обрабатывается до тех пор, пока приложение не будет работать. Приложение не будет простаивать до тех пор, пока рабочий не будет выполнен. (Кроме того, это занятый цикл - отвратительный.)

Другие добавили предложение: kludging его в:

while (_worker.IsBusy)
{
    Application.DoEvents();
}

Проблема заключается в том, что Application.DoEvents() приводит к обработке сообщений, находящихся в очереди в очереди, что вызывает проблемы с повторным подключением (.NET не является повторным).

Я хотел бы использовать какое-то решение, включающее объекты синхронизации событий, где код ждет для события - то, что задает обработчик событий RunWorkerCompleted. Что-то вроде:

Event _workerDoneEvent = new WaitHandle();

public void CancelDoingStuff()
{
    _worker.CancelAsync();
    _workerDoneEvent.WaitOne();
}

private void RunWorkerCompletedEventHandler(sender object, RunWorkerCompletedEventArgs e)
{
    _workerDoneEvent.SetEvent();
}

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

Итак, как вы можете дождаться завершения BackgroundWorker?


Обновление Люди, похоже, смущены этим вопросом. Кажется, они думают, что я буду использовать BackgroundWorker как:

BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += MyWork;
worker.RunWorkerAsync();
WaitForWorkerToFinish(worker);

Это не так, это не то, что я делаю, и это не то, что здесь задают. Если бы это было так, не было бы смысла использовать фонового работника.

Ответ 1

Если я правильно понимаю ваше требование, вы можете сделать что-то вроде этого (код не проверен, но показывает общую идею):

private BackgroundWorker worker = new BackgroundWorker();
private AutoResetEvent _resetEvent = new AutoResetEvent(false);

public Form1()
{
    InitializeComponent();

    worker.DoWork += worker_DoWork;
}

public void Cancel()
{
    worker.CancelAsync();
    _resetEvent.WaitOne(); // will block until _resetEvent.Set() call made
}

void worker_DoWork(object sender, DoWorkEventArgs e)
{
    while(!e.Cancel)
    {
        // do something
    }

    _resetEvent.Set(); // signal that worker is done
}

Ответ 2

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

Второй недостаток заключается в том, что _resetEvent.Set() никогда не будет вызываться, если рабочий поток генерирует исключение - оставив основной поток неопределенным - однако этот недостаток может быть легко исправлен с помощью блока try/finally.

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

Другой метод (при условии, что у вас есть максимум одно окно моделирования открыто) - это установить ActiveForm.Enabled = false, затем цикл на приложении, DoEvents, пока фоновый рабочий не завершит отмену, после чего вы можете установить ActiveForm.Enabled = true еще раз.

Ответ 3

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

Рассмотрим обработчик события RunWorkerComplete:

private void OnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (!e.Cancelled)
    {
        rocketOnPad = false;
        label1.Text = "Rocket launch complete.";
    }
    else
    {
        rocketOnPad = true;
        label1.Text = "Rocket launch aborted.";
    }
    worker = null;
}

И все хорошо.

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

private void BlowUpRocket()
{
    if (worker != null)
    {
        worker.CancelAsync();
        WaitForWorkerToFinish(worker);
        worker = null;
    }

    StartClaxon();
    SelfDestruct();
}

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

private void OpenAccessGates()
{
    if (worker != null)
    {
        worker.CancelAsync();
        WaitForWorkerToFinish(worker);
        worker = null;
    }

    if (!rocketOnPad)
        DisengageAllGateLatches();
}

И, наконец, нам нужно растопить ракету, но это не разрешено во время обратного отсчета:

private void DrainRocket()
{
    if (worker != null)
    {
        worker.CancelAsync();
        WaitForWorkerToFinish(worker);
        worker = null;
    }

    if (rocketOnPad)
        OpenFuelValves();
}

Без возможности ждать отмены работника, мы должны перенести все три метода в RunWorkerCompletedEvent:

private void OnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (!e.Cancelled)
    {
        rocketOnPad = false;
        label1.Text = "Rocket launch complete.";
    }
    else
    {
        rocketOnPad = true;
        label1.Text = "Rocket launch aborted.";
    }
    worker = null;

    if (delayedBlowUpRocket)
        BlowUpRocket();
    else if (delayedOpenAccessGates)
        OpenAccessGates();
    else if (delayedDrainRocket)
        DrainRocket();
}

private void BlowUpRocket()
{
    if (worker != null)
    {
        delayedBlowUpRocket = true;
        worker.CancelAsync();
        return;
    }

    StartClaxon();
    SelfDestruct();
}

private void OpenAccessGates()
{
    if (worker != null)
    {
        delayedOpenAccessGates = true;
        worker.CancelAsync();
        return;
    }

    if (!rocketOnPad)
        DisengageAllGateLatches();
}

private void DrainRocket()
{
    if (worker != null)
    {
        delayedDrainRocket = true;
        worker.CancelAsync();
        return;
    }

    if (rocketOnPad)
        OpenFuelValves();
}

Теперь я мог бы написать свой код, но я просто не собираюсь. Мне все равно, просто нет.

Ответ 4

Вы можете проверить RunWorkerCompletedEventArgs в RunWorkerCompletedEventHandler, чтобы узнать, что такое статус. Успех, аннулирование или ошибка.

private void RunWorkerCompletedEventHandler(sender object, RunWorkerCompletedEventArgs e)
{
    if(e.Cancelled)
    {
        Console.WriteLine("The worker was cancelled.");
    }
}

Обновить. Чтобы узнать, вызвал ли ваш рабочий стол .CancelAsync(), используя это:

if (_worker.CancellationPending)
{
    Console.WriteLine("Cancellation is pending, no need to call CancelAsync again");
}

Ответ 5

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

Если вы хотите подождать, чтобы что-то завершило, используйте другую конструкцию потоков, которая предоставляет WaitHandle.

Ответ 6

Почему вы не можете просто привязываться к событию BackgroundWorker.RunWorkerCompleted. Это обратный вызов, который будет "Происходить при завершении фоновой операции, был отменен или вызвал исключение".

Ответ 7

Я не понимаю, почему вы хотите дождаться завершения BackgroundWorker; это действительно кажется полной противоположностью мотивации для класса.

Однако вы можете запускать каждый метод с вызовом worker.IsBusy и выходить из него, если он запущен.

Ответ 8

Просто хочу сказать, что я пришел сюда, потому что мне нужен фоновой работник, чтобы ждать, пока я запускаю процесс асинхронной работы, в то время как в цикле мое исправление было проще, чем все это другое. ^^

foreach(DataRow rw in dt.Rows)
{
     //loop code
     while(!backgroundWorker1.IsBusy)
     {
         backgroundWorker1.RunWorkerAsync();
     }
}

Просто подумал, что я поделюсь, потому что именно там я оказался в поиске решения. Кроме того, это мой первый пост при переполнении стека, поэтому, если это плохо или что-то, что я бы хотел критиков!:)

Ответ 9

Возможно, я не прав, ваш вопрос.

Фоновый работник вызывает событие WorkerCompleted после его "workmethod" (метод/функция/sub, который обрабатывает backgroundworker.doWork-event) завершена, поэтому нет необходимости проверять, работает ли BW. Если вы хотите остановить своего работника, проверьте отложенное свойство отсрочки внутри вашего "рабочего метода".

Ответ 10

Рабочий процесс объекта BackgroundWorker в основном требует, чтобы вы обрабатывали событие RunWorkerCompleted как для обычных случаев использования, так и для случаев аннулирования пользователя. Вот почему существует свойство RunWorkerCompletedEventArgs.Cancelled. В принципе, для этого необходимо, чтобы ваш метод Отмена считался асинхронным методом сам по себе.

Вот пример:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.ComponentModel;

namespace WindowsFormsApplication1
{
    public class AsyncForm : Form
    {
        private Button _startButton;
        private Label _statusLabel;
        private Button _stopButton;
        private MyWorker _worker;

        public AsyncForm()
        {
            var layoutPanel = new TableLayoutPanel();
            layoutPanel.Dock = DockStyle.Fill;
            layoutPanel.ColumnStyles.Add(new ColumnStyle());
            layoutPanel.ColumnStyles.Add(new ColumnStyle());
            layoutPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
            layoutPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));

            _statusLabel = new Label();
            _statusLabel.Text = "Idle.";
            layoutPanel.Controls.Add(_statusLabel, 0, 0);

            _startButton = new Button();
            _startButton.Text = "Start";
            _startButton.Click += HandleStartButton;
            layoutPanel.Controls.Add(_startButton, 0, 1);

            _stopButton = new Button();
            _stopButton.Enabled = false;
            _stopButton.Text = "Stop";
            _stopButton.Click += HandleStopButton;
            layoutPanel.Controls.Add(_stopButton, 1, 1);

            this.Controls.Add(layoutPanel);
        }

        private void HandleStartButton(object sender, EventArgs e)
        {
            _stopButton.Enabled = true;
            _startButton.Enabled = false;

            _worker = new MyWorker() { WorkerSupportsCancellation = true };
            _worker.RunWorkerCompleted += HandleWorkerCompleted;
            _worker.RunWorkerAsync();

            _statusLabel.Text = "Running...";
        }

        private void HandleStopButton(object sender, EventArgs e)
        {
            _worker.CancelAsync();
            _statusLabel.Text = "Cancelling...";
        }

        private void HandleWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Cancelled)
            {
                _statusLabel.Text = "Cancelled!";
            }
            else
            {
                _statusLabel.Text = "Completed.";
            }

            _stopButton.Enabled = false;
            _startButton.Enabled = true;
        }

    }

    public class MyWorker : BackgroundWorker
    {
        protected override void OnDoWork(DoWorkEventArgs e)
        {
            base.OnDoWork(e);

            for (int i = 0; i < 10; i++)
            {
                System.Threading.Thread.Sleep(500);

                if (this.CancellationPending)
                {
                    e.Cancel = true;
                    e.Result = false;
                    return;
                }
            }

            e.Result = true;
        }
    }
}

Если вы действительно не хотите, чтобы ваш метод вышел, я бы предложил поставить флаг как AutoResetEvent на производный BackgroundWorker, а затем переопределить OnRunWorkerCompleted, чтобы установить флаг. Тем не менее, это еще что-то вроде kludgy; Я бы рекомендовал обработать событие отмены как асинхронный метод и делать то, что он делает в обработчике RunWorkerCompleted.

Ответ 11

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

Что-то вроде этого:

class Test : Form
{
    private BackgroundWorker MyWorker = new BackgroundWorker();

    public Test() {
        MyWorker.DoWork += new DoWorkEventHandler(MyWorker_DoWork);
    }

    void MyWorker_DoWork(object sender, DoWorkEventArgs e) {
        for (int i = 0; i < 100; i++) {
            //Do stuff here
            System.Threading.Thread.Sleep((new Random()).Next(0, 1000));  //WARN: Artificial latency here
            if (MyWorker.CancellationPending) { return; } //Bail out if MyWorker is cancelled
        }
    }

    public void CancelWorker() {
        if (MyWorker != null && MyWorker.IsBusy) {
            MyWorker.CancelAsync();
            System.Threading.ThreadStart WaitThread = new System.Threading.ThreadStart(delegate() {
                while (MyWorker.IsBusy) {
                    System.Threading.Thread.Sleep(100);
                }
            });
            WaitThread.BeginInvoke(a => {
                Invoke((MethodInvoker)delegate() { //Invoke your StuffAfterCancellation call back onto the UI thread
                    StuffAfterCancellation();
                });
            }, null);
        } else {
            StuffAfterCancellation();
        }
    }

    private void StuffAfterCancellation() {
        //Things to do after MyWorker is cancelled
    }
}

В сущности, это то, что это происходит, - это запустить другой поток для запуска в фоновом режиме, который просто ждет в нем цикла занятости, чтобы проверить, завершено ли MyWorker. Как только MyWorker завершит отмену, поток завершится, и мы сможем использовать его AsyncCallback для выполнения любого метода, который нам нужен, чтобы следовать за успешной отменой - он будет работать как psuedo-событие. Так как это отдельно от потока пользовательского интерфейса, он не будет блокировать пользовательский интерфейс, пока мы ждем, когда MyWorker завершит отмену. Если ваше намерение действительно заключается в блокировке и ожидании отмены, это бесполезно для вас, но если вы просто хотите подождать, чтобы начать другой процесс, тогда это будет хорошо.

Ответ 12

Imports System.Net
Imports System.IO
Imports System.Text

Public Class Form1
   Dim f As New Windows.Forms.Form
  Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
   BackgroundWorker1.WorkerReportsProgress = True
    BackgroundWorker1.RunWorkerAsync()
    Dim l As New Label
    l.Text = "Please Wait"
    f.Controls.Add(l)
    l.Dock = DockStyle.Fill
    f.StartPosition = FormStartPosition.CenterScreen
    f.FormBorderStyle = Windows.Forms.FormBorderStyle.None
    While BackgroundWorker1.IsBusy
        f.ShowDialog()
    End While
End Sub




Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork

    Dim i As Integer
    For i = 1 To 5
        Threading.Thread.Sleep(5000)
        BackgroundWorker1.ReportProgress((i / 5) * 100)
    Next
End Sub

Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
    Me.Text = e.ProgressPercentage

End Sub

 Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted

    f.Close()

End Sub

End Class

Ответ 13

Я знаю, что это очень поздно (5 лет), но вы ищете использовать Thread и SynchronizationContext. Вам нужно будет маршировать вызовы пользовательского интерфейса обратно в поток пользовательского интерфейса "вручную", а не позволять Framework делать это автоматически.

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

Ответ 14

Решение Fredrik Kalseth для этой проблемы - лучшее, что я нашел до сих пор. В других решениях используется Application.DoEvent(), что может вызвать проблемы или просто не работать. Позвольте мне перевести его решение в класс многоразового использования. Поскольку BackgroundWorker не запечатан, мы можем извлечь из него наш класс:

public class BackgroundWorkerEx : BackgroundWorker
{
    private AutoResetEvent _resetEvent = new AutoResetEvent(false);
    private bool _resetting, _started;
    private object _lockObject = new object();

    public void CancelSync()
    {
        bool doReset = false;
        lock (_lockObject) {
            if (_started && !_resetting) {
                _resetting = true;
                doReset = true;
            }
        }
        if (doReset) {
            CancelAsync();
            _resetEvent.WaitOne();
            lock (_lockObject) {
                _started = false;
                _resetting = false;
            }
        }
    }

    protected override void OnDoWork(DoWorkEventArgs e)
    {
        lock (_lockObject) {
            _resetting = false;
            _started = true;
            _resetEvent.Reset();
        }
        try {
            base.OnDoWork(e);
        } finally {
            _resetEvent.Set();
        }
    }
}

С флагами и правильной блокировкой мы гарантируем, что _resetEvent.WaitOne() действительно вызывается только при запуске какой-либо работы, иначе _resetEvent.Set(); никогда не вызывается!

Try-finally гарантирует, что будет вызываться _resetEvent.Set();, даже если в нашем DoWork-обработчике должно произойти исключение. В противном случае приложение может зависнуть навсегда при вызове CancelSync!

Мы будем использовать его следующим образом:

BackgroundWorkerEx _worker;

void StartWork()
{
    StopWork();
    _worker = new BackgroundWorkerEx { 
        WorkerSupportsCancellation = true,
        WorkerReportsProgress = true
    };
    _worker.DoWork += Worker_DoWork;
    _worker.ProgressChanged += Worker_ProgressChanged;
}

void StopWork()
{
    if (_worker != null) {
        _worker.CancelSync(); // Use our new method.
    }
}

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
    for (int i = 1; i <= 20; i++) {
        if (worker.CancellationPending) {
            e.Cancel = true;
            break;
        } else {
            // Simulate a time consuming operation.
            System.Threading.Thread.Sleep(500);
            worker.ReportProgress(5 * i);
        }
    }
}

private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    progressLabel.Text = e.ProgressPercentage.ToString() + "%";
}

Вы также можете добавить обработчик к событию RunWorkerCompleted, как показано ниже:
    BackgroundWorker Class (документация Microsoft).

Ответ 15

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

Почему это так сложно?

Простой Thread.Sleep(1500) работает, но он задерживает выключение (если слишком долго) или вызывает исключения (если они слишком короткие).

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

private volatile bool bwRunning = false;

...

private void MainWin_FormClosing(Object sender, FormClosingEventArgs e)
{
    ... // Clean house as-needed.

    bwInstance.CancelAsync();  // Flag background worker to stop.
    while (bwRunning)
        Thread.Sleep(100);  // Wait for background worker to stop.
}  // (The form really gets closed now.)

...

private void bwBody(object sender, DoWorkEventArgs e)
{
    bwRunning = true;

    BackgroundWorker bw = sender as BackgroundWorker;

    ... // Set up (open logfile, etc.)

    for (; ; )  // infinite loop
    {
        ...
        if (bw.CancellationPending) break;
        ...
    } 

    ... // Tear down (close logfile, etc.)

    bwRunning = false;
}  // (bwInstance dies now.)

Ответ 16

Вы можете отключиться от события RunWorkerCompleted. Даже если вы уже добавили обработчик событий для _worker, вы можете добавить еще один, который они будут выполнять в том порядке, в котором они были добавлены.

public class DoesStuff
{
    BackgroundWorker _worker = new BackgroundWorker();

    ...

    public void CancelDoingStuff()
    {
        _worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler((sender, e) => 
        {
            // do whatever you want to do when the cancel completes in here!
        });
        _worker.CancelAsync();
    }
}

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

void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    if (_worker != null)
    {
        _worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler((sender3, e3) => this.Close());
        _worker.CancelAsync();
        e.Cancel = true;
    }
}

Ответ 17

О, человек, некоторые из них стали смехотворно сложными. все, что вам нужно сделать, это проверить свойство BackgroundWorker.CancellationPending внутри обработчика DoWork. вы можете проверить его в любое время. после его ожидания установите e.Cancel = True и залог из метода.

//метод здесь private void Worker_DoWork (отправитель объекта, DoWorkEventArgs e) {   BackgroundWorker bw = (отправитель в качестве BackgroundWorker);

// do stuff

if(bw.CancellationPending)
{
    e.Cancel = True;
    return;
}

// do other stuff

}