Как ждать отмены BackgroundWorker? - PullRequest
       80

Как ждать отмены BackgroundWorker?

117 голосов
/ 24 сентября 2008

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

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, и это событие не может быть обработано, пока приложение не перейдет в режим ожидания. Приложение не будет бездействовать, пока рабочий не будет сделан. (Плюс, это занятая петля - отвратительно.)

Другие предложили добавить его в:

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);

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

Ответы [ 18 ]

121 голосов
/ 24 сентября 2008

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

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
}
13 голосов
/ 24 сентября 2008

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

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

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

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

10 голосов
/ 24 сентября 2008

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

Рассмотрим обработчик события 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 голосов
/ 24 сентября 2008

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

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

4 голосов
/ 24 сентября 2008

Вы можете проверить в 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");
}
3 голосов
/ 24 сентября 2008

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

1 голос
/ 18 июня 2015

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

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

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

1 голос
/ 24 сентября 2008

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

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

0 голосов
/ 29 апреля 2017

Закрытие формы закрывает мой открытый лог-файл. Мой фоновый работник записывает этот файл журнала, поэтому я не могу позволить 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.)
0 голосов
/ 27 июня 2014

Решение этой проблемы Фредриком Калсетом - лучшее, что я нашел до сих пор. Другие решения используют 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) .

...