Considérez une méthode hypothétique d'un objet qui remplit des fonctions pour vous:
public class DoesStuff
{
BackgroundWorker _worker = new BackgroundWorker();
...
public void CancelDoingStuff()
{
_worker.CancelAsync();
//todo: Figure out a way to wait for BackgroundWorker to be cancelled.
}
}
Comment peut-on attendre qu'un BackgroundWorker soit terminé?
Dans le passé, les gens ont essayé:
while (_worker.IsBusy)
{
Sleep(100);
}
Mais cet interblocages , car IsBusy
n'est pas effacé tant que l'événement RunWorkerCompleted
n'a pas été traité, et cet événement ne peut pas être traité tant que l'application n'est pas devenue inactive. L'application ne reste pas inactive jusqu'à ce que le travail soit terminé. (De plus, c'est une boucle occupée - dégoûtante.)
D'autres ont ajouté suggérant le masquer dans:
while (_worker.IsBusy)
{
Application.DoEvents();
}
Le problème, c’est que Application.DoEvents()
entraîne le traitement des messages actuellement dans la file d’attente, ce qui entraîne des problèmes de ré-entrée (.NET n’est pas ré-entrant).
J'espère pouvoir utiliser une solution impliquant des objets de synchronisation d'événements, dans laquelle le code attend un événement - défini par les gestionnaires d'événement RunWorkerCompleted
du travailleur. Quelque chose comme:
Event _workerDoneEvent = new WaitHandle();
public void CancelDoingStuff()
{
_worker.CancelAsync();
_workerDoneEvent.WaitOne();
}
private void RunWorkerCompletedEventHandler(sender object, RunWorkerCompletedEventArgs e)
{
_workerDoneEvent.SetEvent();
}
Mais je reviens à l'impasse: le gestionnaire d'événements ne peut pas s'exécuter tant que l'application n'est pas inactive, et l'application ne le restera pas car elle attend un événement.
Alors, comment pouvez-vous attendre qu'un BackgroundWorker se termine?
Update Cette question semble semer la confusion chez les gens. Ils semblent penser que j'utiliserai BackgroundWorker comme:
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += MyWork;
worker.RunWorkerAsync();
WaitForWorkerToFinish(worker);
C'est pas ça, c'est pas ce que je fais et c'est pas ce qui est demandé ici. Si tel était le cas, il ne servirait à rien d'avoir recours à un travailleur d'arrière-plan.
Si je comprends bien votre exigence, vous pouvez faire quelque chose comme ceci (code non testé, mais montre l'idée générale):
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
}
Il y a un problème avec this response. L’interface utilisateur doit continuer à traiter les messages pendant que vous attendez, sinon elle ne sera pas repeinte, ce qui posera un problème si votre agent d’arrière-plan met longtemps à répondre à la demande d’annulation.
Une deuxième faille est que _resetEvent.Set()
ne sera jamais appelé si le thread de travail lève une exception - laissant le thread principal attendre indéfiniment - mais cette faille pourrait facilement être corrigée avec un bloc try/finally.
Pour ce faire, vous pouvez, par exemple, afficher une boîte de dialogue modale avec une minuterie qui vérifie de manière répétée si l’agent d’arrière-plan a terminé son travail (ou l’annulation terminée dans votre cas). Une fois que l’agent d’arrière-plan a terminé, la boîte de dialogue modale renvoie le contrôle à votre application. L'utilisateur ne peut pas interagir avec l'interface utilisateur jusqu'à ce que cela se produise.
Une autre méthode (en supposant que vous ayez ouvert au maximum une fenêtre de modèle) consiste à définir ActiveForm.Enabled = false, puis à effectuer une boucle sur Application, DoEvents jusqu'à ce que l'agent en arrière-plan ait terminé son annulation, après quoi vous pourrez définir ActiveForm.Enabled = true.
Vous êtes presque tous déconcertés par la question et vous ne comprenez pas comment un travailleur est utilisé.
Considérons un gestionnaire d'événements 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;
}
Et tout va bien.
Vient maintenant une situation dans laquelle l'appelant doit abandonner le compte à rebours car il doit exécuter une autodestruction d'urgence de la fusée.
private void BlowUpRocket()
{
if (worker != null)
{
worker.CancelAsync();
WaitForWorkerToFinish(worker);
worker = null;
}
StartClaxon();
SelfDestruct();
}
Et il y a aussi une situation où nous devons ouvrir les portes d'accès à la fusée, mais pas pendant le compte à rebours:
private void OpenAccessGates()
{
if (worker != null)
{
worker.CancelAsync();
WaitForWorkerToFinish(worker);
worker = null;
}
if (!rocketOnPad)
DisengageAllGateLatches();
}
Et enfin, nous devons vider la fusée, mais ce n’est pas permis lors d’un compte à rebours:
private void DrainRocket()
{
if (worker != null)
{
worker.CancelAsync();
WaitForWorkerToFinish(worker);
worker = null;
}
if (rocketOnPad)
OpenFuelValves();
}
Sans la possibilité d’attendre l’annulation d’un travailleur, nous devons déplacer les trois méthodes vers le 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();
}
Maintenant, je pourrais écrire mon code comme ça, mais je ne vais tout simplement pas. Je m'en fiche, je ne le suis pas.
Vous pouvez vérifier dans RunWorkerCompletedEventArgs dans/ RunWorkerCompletedEventHandler pour voir quel était le statut. Succès, annulé ou une erreur.
private void RunWorkerCompletedEventHandler(sender object, RunWorkerCompletedEventArgs e)
{
if(e.Cancelled)
{
Console.WriteLine("The worker was cancelled.");
}
}
Update : Pour voir si votre travailleur a appelé .CancelAsync () en utilisant ceci:
if (_worker.CancellationPending)
{
Console.WriteLine("Cancellation is pending, no need to call CancelAsync again");
}
Vous n'attendez pas que le travail d'arrière-plan soit terminé. Cela va à l’encontre du but de lancer un thread séparé. Au lieu de cela, vous devriez laisser votre méthode se terminer et déplacer tout code dépendant de son achèvement vers un endroit différent. Vous laissez le travailleur vous dire quand c'est fait et appelez ensuite le code restant.
Si vous voulez attendre que quelque chose soit terminé, utilisez une construction de thread différente qui fournit un WaitHandle.
Pourquoi ne pouvez-vous pas simplement vous lier à l'événement BackgroundWorker.RunWorkerCompleted. C'est un rappel qui "se produira lorsque l'opération en arrière-plan sera terminée, annulée ou aura déclenché une exception".
Je ne comprends pas pourquoi vous voudriez attendre qu'un BackgroundWorker soit complet; cela ressemble vraiment à l'opposé de la motivation de la classe.
Cependant, vous pouvez démarrer chaque méthode avec un appel à worker.IsBusy et les laisser se fermer si elle est en cours d'exécution.
Je veux juste dire que je suis venu ici parce que j'ai besoin d'un agent d'arrière-plan pour attendre pendant que j'exécutais un processus asynchrone alors que j'étais en boucle, ma solution était bien plus simple que toutes ces autres choses ^^
foreach(DataRow rw in dt.Rows)
{
//loop code
while(!backgroundWorker1.IsBusy)
{
backgroundWorker1.RunWorkerAsync();
}
}
J'ai juste pensé partager, car c'est là que je me suis retrouvé alors que je cherchais une solution. De plus, ceci est mon premier article sur le débordement de pile, donc si c'est mauvais ou quoi que ce soit, j'adorerais les critiques! :)
J'utilise les méthodes async
et await
pour attendre que le travailleur termine son travail:
public async Task StopAsync()
{
_worker.CancelAsync();
while (_isBusy)
await Task.Delay(1);
}
et dans la méthode DoWork
:
public async Task DoWork()
{
_isBusy = true;
while (!_worker.CancellationPending)
{
// Do something.
}
_isBusy = false;
}
Vous pouvez également encapsuler la boucle while
dans DoWork
avec try ... catch
pour définir _isBusy
est false
à l'exception. Ou bien, cochez simplement _worker.IsBusy
dans la boucle StopAsync
while.
Voici un exemple de mise en œuvre complète:
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;
}
}
Pour arrêter le travailleur et attendre qu'il soit terminé:
await myBackgroundWorker.StopAsync();
Les problèmes avec cette méthode sont:
Hm peut-être que je ne comprends pas bien votre question.
Le backgroundworker appelle l'événement WorkerCompleted une fois que son 'workermethod' (la méthode/fonction/sous qui gère le backgroundworker.doWork-event ) est terminé, il n'est donc pas nécessaire de vérifier si le BW est toujours en cours d'exécution . Si vous souhaitez arrêter votre opérateur, vérifiez la propriété annulation en attente à l'intérieur de votre 'méthode d'ouvrier'.
La solution de Fredrik Kalseth à ce problème est la meilleure que j'ai trouvée jusqu'à présent. D'autres solutions utilisent Application.DoEvent()
qui peuvent causer des problèmes ou tout simplement ne fonctionnent pas. Permettez-moi de jeter sa solution dans une classe réutilisable. Puisque BackgroundWorker
n'est pas scellé, nous pouvons en tirer notre classe:
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();
}
}
}
Avec des drapeaux et un verrouillage correct, nous nous assurons que _resetEvent.WaitOne()
ne soit vraiment appelé que si du travail a été démarré, sinon _resetEvent.Set();
pourrait ne jamais être appelé!
Try-finally garantit que _resetEvent.Set();
sera appelé, même si une exception devrait survenir dans notre gestionnaire DoWork. Sinon, l'application pourrait se figer pour toujours lors de l'appel de CancelSync
!
Nous l'utiliserions comme ceci:
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() + "%";
}
Vous pouvez également ajouter un gestionnaire à l'événement RunWorkerCompleted
, comme indiqué ici:
Classe BackgroundWorker(documentation Microsoft).
Fermer le formulaire ferme mon fichier de log ouvert. Mon arrière-plan écrit ce fichier journal, je ne peux donc pas laisser MainWin_FormClosing()
terminer jusqu'à ce que mon arrière-plan se termine. Si je n'attends pas que mon agent d'arrière-plan prenne fin, des exceptions se produisent.
Pourquoi est-ce si difficile?
Une simple Thread.Sleep(1500)
fonctionne, mais elle retarde l'arrêt (si elle est trop longue) ou provoque des exceptions (si elle est trop courte).
Pour arrêter juste après la fin du travail de l’arrière-plan, utilisez simplement une variable. Cela fonctionne pour moi:
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.)
Je sais que c'est vraiment tard (5 ans) mais ce que vous cherchez, c'est d'utiliser un thread et un SynchronizationContext . Vous devrez marshaler les appels d'interface utilisateur dans le fil d'interface utilisateur "à la main" plutôt que de laisser le Framework le faire automatiquement.
Cela vous permet d'utiliser un fil que vous pouvez attendre si besoin est.
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
Vous pouvez tirer parti de l'événement RunWorkerCompleted. Même si vous avez déjà ajouté un gestionnaire d'événements pour _worker, vous pouvez en ajouter un autre qu'ils exécuteront dans l'ordre dans lequel ils ont été ajoutés.
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();
}
}
cela peut être utile si vous avez plusieurs raisons pour lesquelles une annulation peut se produire, ce qui rend la logique d'un seul gestionnaire RunWorkerCompleted plus compliquée que vous le souhaitez. Par exemple, annuler lorsqu'un utilisateur essaie de fermer le formulaire:
void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (_worker != null)
{
_worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler((sender, e) => this.Close());
_worker.CancelAsync();
e.Cancel = true;
}
}
Le flux de travail d'un objet BackgroundWorker
nécessite essentiellement de gérer l'événement RunWorkerCompleted
pour les cas d'utilisation d'exécution normale et d'annulation d'utilisateur. C'est pourquoi la propriété RunWorkerCompletedEventArgs.Cancelled existe. Pour le faire correctement, vous devez fondamentalement considérer que votre méthode Cancel est une méthode asynchrone en soi.
Voici un exemple:
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 vous vraiment vraiment ne voulez pas que votre méthode se ferme, nous vous suggérons de placer un indicateur du type AutoResetEvent
sur un BackgroundWorker
dérivé, puis de remplacer OnRunWorkerCompleted
pour le définir. C'est quand même un peu bavard; Je recommanderais de traiter l'événement cancel comme une méthode asynchrone et de faire ce qu'il fait actuellement dans le gestionnaire RunWorkerCompleted
.
Je suis un peu en retard pour la fête ici (environ 4 ans), mais qu'en est-il de la configuration d'un thread asynchrone pouvant gérer une boucle occupée sans verrouiller l'interface utilisateur, le rappel de ce thread étant alors la confirmation que l'annulation de BackgroundWorker est terminée ?
Quelque chose comme ça:
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
}
}
Essentiellement, cela déclenche un autre thread à exécuter en arrière-plan qui attend simplement dans sa boucle occupée pour voir si la MyWorker
est terminée. Une fois que MyWorker
a fini d'annuler le thread, il se fermera et nous pourrons utiliser la méthode AsyncCallback
pour exécuter la méthode dont nous avons besoin pour suivre l'annulation réussie. Cela fonctionnera comme un événement psuedo. Comme il s'agit d'un processus distinct du thread d'interface utilisateur, il ne verrouillera pas l'interface utilisateur tant que nous attendrons que MyWorker
finisse de l'annuler. Si votre intention est vraiment de verrouiller et d’attendre l’annulation, cela ne vous servira à rien, mais si vous voulez juste attendre pour pouvoir lancer un autre processus, cela fonctionnera bien.