Как ждать отмены 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
событие обрабатывается, и это событие не может быть обработано, пока приложение не будет бездействующим. Приложение не будет бездействовать, пока рабочий не будет сделан. (Плюс, это занятая петля - отвратительно.)
Другие добавили, предложили добавить это в:
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);
Это не так, это не то, что я делаю, и это не то, что здесь спрашивают. Если бы это было так, не было бы смысла использовать фонового работника.
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, затем выполнить цикл 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();
}
Теперь я мог бы написать свой код таким образом, но я просто не собираюсь. Мне все равно, я просто нет.
Вы не ждете завершения фонового работника. Это в значительной степени противоречит цели запуска отдельного потока. Вместо этого вы должны позволить вашему методу завершиться и переместить любой код, который зависит от завершения, в другое место. Вы позволяете работнику сообщать вам, когда это будет сделано, и затем вызывать любой оставшийся код.
Если вы хотите дождаться завершения чего-либо, используйте другую конструкцию потоков, которая предоставляет WaitHandle.
Вы можете проверить в 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");
}
Почему вы не можете просто присоединиться к событию BackgroundWorker.RunWorkerCompleted. Это обратный вызов: "Происходит, когда фоновая операция завершена, отменена или вызвала исключение".
Просто хочу сказать, что я пришел сюда, потому что мне нужен фоновый работник, чтобы ждать, пока я выполняю асинхронный процесс в цикле, мое исправление было намного проще, чем все эти другие вещи ^^
foreach(DataRow rw in dt.Rows)
{
//loop code
while(!backgroundWorker1.IsBusy)
{
backgroundWorker1.RunWorkerAsync();
}
}
Просто решил, что поделюсь, потому что это то, где я оказался в поисках решения. Кроме того, это мой первый пост о переполнении стека, так что, если это плохо или что-то еще, я бы полюбил критиков!:)
Я не понимаю, почему вы хотите дождаться завершения BackgroundWorker; это действительно кажется полной противоположностью мотивации для класса.
Тем не менее, вы можете запустить каждый метод с вызовом worker.IsBusy и заставить его выйти, если он запущен.
Я немного опоздал на вечеринку здесь (около 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
закончить отмену. Если вы действительно хотите заблокировать и дождаться отмены, тогда это бесполезно для вас, но если вы просто хотите подождать, чтобы запустить другой процесс, это работает хорошо.
Решение Фредрика Калсета этой проблемы - лучшее, что я нашел до сих пор. Другие решения используют 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).
Я использую 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();
Проблемы с этим методом:
- Вы должны использовать асинхронные методы полностью.
- жду Task.Delay неточно. На моем ПК Task.Delay(1) фактически ожидает ~20 мс.
Хм, может быть, я не правильно понял ваш вопрос.
Фоновый работник вызывает событие 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
обработчик.
Закрытие формы закрывает мой открытый лог-файл. Мой фоновый работник пишет этот файл журнала, поэтому я не могу позволить 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;
}
}
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
Я знаю, что это действительно поздно (5 лет), но вам нужно использовать Thread и SynchronizationContext. Вам придется маршалировать вызовы пользовательского интерфейса обратно в поток пользовательского интерфейса "вручную", а не позволять платформе делать это автоматически.
Это позволяет вам использовать поток, который можно подождать, если потребуется.
О человек, некоторые из них стали смехотворно сложными. все, что вам нужно сделать, это проверить свойство 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
}