Как ждать, пока фоновый работник отменит?


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

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 worker = new BackgroundWorker();
worker.DoWork += MyWork;
worker.RunWorkerAsync();
WaitForWorkerToFinish(worker);

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

17 114

17 ответов:

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

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
}

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

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

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

другой метод (предполагая, что у вас есть максимум одно немодальное открытое окно) - установить ActiveForm.Enabled = false, затем loop on Приложение, DoEvents до тех пор, пока фоновый рабочий не закончит отмену, после чего вы можете установить ActiveForm.Включен = истина снова.

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

рассмотрим обработчик событий 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();
}

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

Вы можете проверить в 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");
}

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

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

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

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

тем не менее, вы можете начать каждый метод с вызова worker.IsBusy и имеют их выход, если он работает.

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

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

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

Хм, может быть, я не совсем правильно понял ваш вопрос.

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

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

вот пример:

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; я бы рекомендовал рассматривать событие cancel как асинхронный метод и делать все, что он сейчас делает в RunWorkerCompleted обработчик.

Я немного опоздал на вечеринку здесь (около 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 для завершения отмены. Если ваше намерение действительно состоит в том, чтобы заблокировать и ждать отмены, то это бесполезно для вас, но если вы просто хотите подождать, чтобы вы могли начать другой процесс, то это работает хорошо.

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

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

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

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

попытка-окончательно обеспечивает это _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(документация Microsoft).

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

вы можете piggy обратно от 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;
    }
}

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

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

// do stuff

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

// do other stuff

}