¿Cómo esperar para que un BackgroundWorker se cancele?

Considere un método hipotético de un objeto que hace cosas por usted:

public class DoesStuff { BackgroundWorker _worker = new BackgroundWorker(); ... public void CancelDoingStuff() { _worker.CancelAsync(); //todo: Figure out a way to wait for BackgroundWorker to be cancelled. } } 

¿Cómo se puede esperar a que se haga un BackgroundWorker?


En el pasado, la gente ha intentado:

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

Pero esto se traba , porque IsBusy no se borra hasta que se maneja el evento RunWorkerCompleted , y ese evento no se puede manejar hasta que la aplicación se encuentre inactiva. La aplicación no estará inactiva hasta que el trabajador termine. (Además, es un ciclo ocupado, repugnante).

Otros han agregado sugerido kludging en:

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

El problema con eso es que Application.DoEvents() hace que los mensajes actualmente en la cola se procesen, lo que causa problemas de reentrada (.NET no es reentrante).

Espero utilizar alguna solución que implique objetos de sincronización de eventos, donde el código espera un evento, que los controladores de eventos RunWorkerCompleted del trabajador establezcan. Algo como:

 Event _workerDoneEvent = new WaitHandle(); public void CancelDoingStuff() { _worker.CancelAsync(); _workerDoneEvent.WaitOne(); } private void RunWorkerCompletedEventHandler(sender object, RunWorkerCompletedEventArgs e) { _workerDoneEvent.SetEvent(); } 

Pero volví al punto muerto: el controlador de eventos no puede ejecutarse hasta que la aplicación esté inactiva y la aplicación no estará inactiva porque está esperando un evento.

Entonces, ¿cómo puedes esperar para que un BackgroundWorker termine?


Actualización La gente parece confundida por esta pregunta. Parecen pensar que voy a utilizar el BackgroundWorker como:

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

Eso no es, eso no es lo que estoy haciendo, y eso no es lo que se está preguntando aquí. Si ese fuera el caso, no tendría sentido usar un trabajador de segundo plano.

Si entiendo bien su requisito, podría hacer algo como esto (código no probado, pero muestra la idea general):

 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 } 

Hay un problema con esta respuesta. La IU debe continuar procesando los mensajes mientras espera; de lo contrario, no se volverá a pintar, lo que será un problema si el técnico de soporte tarda mucho tiempo en responder a la solicitud de cancelación.

Una segunda falla es que _resetEvent.Set() nunca se _resetEvent.Set() si el hilo del trabajador arroja una excepción, dejando el hilo principal esperando indefinidamente, sin embargo, este error podría solucionarse fácilmente con un bloque try / finally.

Una forma de hacerlo es mostrar un cuadro de diálogo modal que tiene un temporizador que comprueba repetidamente si el trabajador en segundo plano ha terminado el trabajo (o ha terminado de cancelar en su caso). Una vez que el trabajador de fondo ha terminado, el diálogo modal devuelve control a su aplicación. El usuario no puede interactuar con la interfaz de usuario hasta que esto suceda.

Otro método (suponiendo que tenga un máximo de una ventana no modal abierta) es establecer ActiveForm.Enabled = false, luego realizar un bucle en Application, DoEvents hasta que el trabajador en segundo plano haya terminado de cancelar, después de lo cual puede establecer ActiveForm.Enabled = true nuevamente.

Casi todos ustedes están confundidos por la pregunta y no están entendiendo cómo se usa a un trabajador.

Considere un controlador de eventos 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; } 

Y todo está bien.

Ahora viene una situación donde la persona que llama debe abortar la cuenta regresiva porque necesitan ejecutar una autodestrucción de emergencia del cohete.

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

Y también hay una situación en la que tenemos que abrir las puertas de acceso al cohete, pero no al hacer una cuenta atrás:

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

Y finalmente, necesitamos desenergizar el cohete, pero eso no está permitido durante una cuenta atrás:

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

Sin la capacidad de esperar que un trabajador cancele, debemos mover los tres métodos al 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(); } 

Ahora podría escribir mi código así, pero no lo haré. No me importa, simplemente no lo estoy.

Puede consultar en RunWorkerCompletedEventArgs en RunWorkerCompletedEventHandler para ver cuál era el estado. Éxito, cancelado o un error

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

Actualización : para ver si su trabajador ha llamado .CancelAsync () usando esto:

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

No espere a que el trabajador de segundo plano complete. Eso prácticamente frustra el propósito de lanzar un hilo por separado. En su lugar, debe dejar que su método finalice, y mover cualquier código que depende de la finalización a un lugar diferente. Deje que el trabajador le diga cuándo termina y llame a cualquier código restante.

Si desea esperar que algo se complete, utilice una construcción de subprocesamiento diferente que proporcione un WaitHandle.

¿Por qué no puedes unirme al evento BackgroundWorker.RunWorkerCompleted? Es una callback que “ocurrirá cuando la operación en segundo plano se haya completado, se haya cancelado o haya generado una excepción”.

No entiendo por qué querría esperar a que se complete un BackgroundWorker; realmente parece ser exactamente lo opuesto a la motivación de la clase.

Sin embargo, puede iniciar cada método con una llamada a worker.IsBusy y hacer que salgan si se está ejecutando.

Solo quiero decir que vine aquí porque necesito que un trabajador de segundo plano espere mientras estaba ejecutando un proceso de sincronización mientras está en un bucle, mi solución fue mucho más fácil que todas estas otras cosas ^^

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

Pensé que compartiría porque aquí es donde terminé mientras buscaba una solución. Además, esta es mi primera publicación sobre el desbordamiento de la stack, así que si es malo o algo, ¡me encantaría la crítica! 🙂

Hm, tal vez no entiendo bien tu pregunta.

El trabajador en segundo plano llama al evento WorkerCompleted una vez que su ‘workermethod’ (el método / función / sub que maneja el backgroundworker.doWork-event ) finaliza, por lo que no es necesario verificar si el BW aún se está ejecutando. Si desea detener a su trabajador, verifique la propiedad de cancelación pendiente dentro de su ‘método de trabajador’.

El flujo de trabajo de un objeto BackgroundWorker básicamente requiere que usted maneje el evento RunWorkerCompleted tanto para la ejecución normal como para los casos de uso de cancelación del usuario. Esta es la razón por la cual existe la propiedad RunWorkerCompletedEventArgs.Cancelled . Básicamente, hacer esto correctamente requiere que consideres que tu método Cancelar es un método asíncrono en sí mismo.

Aquí hay un ejemplo:

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

Si realmente no quieres que tu método salga, te sugiero poner una bandera como AutoResetEvent en un BackgroundWorker derivado, y luego anular OnRunWorkerCompleted para establecer la bandera. Sin embargo, todavía es un poco kludgy; Recomiendo tratar el evento cancelar como un método asincrónico y hacer lo que esté haciendo actualmente en el controlador RunWorkerCompleted .

Llego un poco tarde a la fiesta aquí (alrededor de 4 años) pero ¿qué hay de configurar un hilo asíncrono que pueda manejar un bucle ocupado sin bloquear la interfaz de usuario, y luego hacer que la callback de ese hilo sea la confirmación de que BackgroundWorker ha terminado de cancelar ?

Algo como esto:

 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 } } 

En esencia, lo que hace es disparar otro hilo para ejecutar en segundo plano que solo espera en su ciclo de ocupado para ver si MyWorker ha completado. Una vez que MyWorker haya terminado de cancelar, el hilo se cerrará y podremos usar su AsyncCallback para ejecutar cualquier método que necesitemos para seguir la cancelación exitosa. Funcionará como un evento psuedo. Como esto está separado del subproceso UI, no bloqueará la UI mientras esperamos que MyWorker termine de cancelarse. Si tu intención es bloquear y esperar la cancelación, entonces esto no sirve para nada, pero si solo quieres esperar para poder comenzar otro proceso, esto funciona bien.

Sé que esto es muy tarde (5 años) pero lo que estás buscando es usar un hilo y un SincronizaciónContexto . Deberá ordenar las llamadas de IU a la secuencia de la interfaz de usuario “a mano” en lugar de dejar que el Framework lo haga automáticamente.

Esto le permite usar un hilo que puede esperar si es necesario.

 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 

La solución de Fredrik Kalseth a este problema es la mejor que he encontrado hasta ahora. Otras soluciones usan Application.DoEvent() que pueden causar problemas o simplemente no funcionan. Déjame convertir su solución en una clase reutilizable. Como BackgroundWorker no está sellado, podemos derivar nuestra clase de él:

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

Con indicadores y un locking adecuado, nos aseguramos de que _resetEvent.WaitOne() realmente solo se llame si se ha iniciado algún trabajo, de lo contrario _resetEvent.Set(); ¡podría nunca haber sido llamado!

El try-finally asegura que _resetEvent.Set(); se llamará, incluso si se produce una excepción en nuestro controlador DoWork. De lo contrario, la aplicación podría congelarse para siempre al llamar a CancelSync .

Lo usaríamos así:

 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() + "%"; } 

También puede agregar un controlador al evento RunWorkerCompleted como se muestra aquí:
Clase BackgroundWorker (documentación de Microsoft) .

Al cerrar el formulario, se cierra mi archivo de registro abierto. Mi trabajador de fondo escribe ese archivo de registro, por lo que no puedo dejar que MainWin_FormClosing() finalice hasta que mi trabajador de fondo termine. Si no espero a que finalice mi trabajador de segundo plano, suceden excepciones.

¿Por qué es tan difícil?

Funciona un Thread.Sleep(1500) simple, pero retrasa el apagado (si es demasiado largo) o causa excepciones (si es demasiado corto).

Para cerrar inmediatamente después de que el trabajador de fondo finalice, simplemente use una variable. Esto es trabajo para mí:

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

Puedes volver al evento RunWorkerCompleted. Incluso si ya ha agregado un controlador de eventos para _worker, puede agregar otro y se ejecutarán en el orden en que se agregaron.

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

esto podría ser útil si tiene varias razones por las cuales puede producirse una cancelación, lo que hace que la lógica de un solo manejador RunWorkerCompleted sea más complicada de lo que usted desea. Por ejemplo, cancelando cuando un usuario intenta cerrar el formulario:

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

oh hombre, algunos de estos se han vuelto ridículamente complejos. todo lo que necesita hacer es verificar la propiedad BackgroundWorker.CancellationPending dentro del controlador DoWork. puedes verificarlo en cualquier momento. una vez que esté pendiente, configure e.Cancel = True y libere del método.

// método aquí private void Worker_DoWork (remitente del objeto, DoWorkEventArgs e) {BackgroundWorker bw = (remitente como BackgroundWorker);

 // do stuff if(bw.CancellationPending) { e.Cancel = True; return; } // do other stuff 

}