Вопрос

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

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

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

Это было полезно?

Решение

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

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, а затем зациклить Application,DoEvents до тех пор, пока фоновый рабочий процесс не завершит отмену, после чего вы можете снова установить ActiveForm.Enabled = true.

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

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

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

Хм, возможно, я не правильно понимаю ваш вопрос.

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

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

Вот пример:

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

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

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

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(); возможно, никогда не позвонят!

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 (документация 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.)

Вы можете использовать событие 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((sender, e) => this.Close());
        _worker.CancelAsync();
        e.Cancel = true;
    }
}

я использую async метод и await дождаться, пока работник завершит свою работу:

    public async Task StopAsync()
    {
        _worker.CancelAsync();

        while (_isBusy)
            await Task.Delay(1);
    }

И в DoWork метод:

    public async Task DoWork()
    {
        _isBusy = true;
        while (!_worker.CancellationPending)
        {
            // Do something.
        }
        _isBusy = false;
    }

Вы также можете инкапсулировать while зациклиться DoWork с try ... catch устанавливать _isBusy является false в порядке исключения.Или просто проверьте _worker.IsBusy в StopAsync пока цикл.

Вот пример полной реализации:

class MyBackgroundWorker
{
    private BackgroundWorker _worker;
    private bool _isBusy;

    public void Start()
    {
        if (_isBusy)
            throw new InvalidOperationException("Cannot start as a background worker is already running.");

        InitialiseWorker();
        _worker.RunWorkerAsync();
    }

    public async Task StopAsync()
    {
        if (!_isBusy)
            throw new InvalidOperationException("Cannot stop as there is no running background worker.");

        _worker.CancelAsync();

        while (_isBusy)
            await Task.Delay(1);

        _worker.Dispose();
    }

    private void InitialiseWorker()
    {
        _worker = new BackgroundWorker
        {
            WorkerSupportsCancellation = true
        };
        _worker.DoWork += WorkerDoWork;
    }

    private void WorkerDoWork(object sender, DoWorkEventArgs e)
    {
        _isBusy = true;
        try
        {
            while (!_worker.CancellationPending)
            {
                // Do something.
            }
        }
        catch
        {
            _isBusy = false;
            throw;
        }

        _isBusy = false;
    }
}

Чтобы остановить работника и дождаться его завершения:

await myBackgroundWorker.StopAsync();

Проблемы этого метода заключаются в следующем:

  1. Вам придется полностью использовать асинхронные методы.
  2. await Task.Delay указан неточно.На моем компьютере Task.Delay(1) фактически ждет ~20 мс.

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

// Метод здесь private void Worker_dowork (отправитель объекта, DoworKeventargs e) {founalworker bw = (отправитель в качестве фонового работника);

// do stuff

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

// do other stuff

}

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top