BackgroundWorker がキャンセルするのを待つにはどうすればよいですか?
-
02-07-2019 - |
質問
を考えてみましょう 仮説的な 代わりに処理を行うオブジェクトのメソッド:
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
}
他のヒント
問題があります これ 応答。待機している間も UI はメッセージの処理を続ける必要があります。そうしないと再描画されません。これは、バックグラウンド ワーカーがキャンセル リクエストに応答するのに長い時間がかかる場合に問題となります。
2つ目の欠陥は、 _resetEvent.Set()
ワーカー スレッドが例外をスローした場合、この関数は決して呼び出されず、メイン スレッドは無期限に待機したままになりますが、この欠陥は try/finally ブロックで簡単に修正できます。
これを行う 1 つの方法は、バックグラウンド ワーカーが作業を終了したか (または、あなたの場合はキャンセルが終了したか) を繰り返し確認するタイマーを備えたモーダル ダイアログを表示することです。バックグラウンド ワーカーが終了すると、モーダル ダイアログは制御をアプリケーションに返します。これが起こるまで、ユーザーは UI を操作できません。
別の方法 (最大 1 つのモードレス ウィンドウを開いていると仮定) は、ActiveForm.Enabled = false に設定し、バックグラウンド ワーカーのキャンセルが完了するまで Application,DoEvents をループします。その後、再度 ActiveForm.Enabled = true に設定できます。
ほぼ全員がこの質問に困惑しており、worker がどのように使用されるかを理解していません。
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();
}
ワーカーがキャンセルするのを待つことができない場合、3 つのメソッドをすべて 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();
}
}
解決策を探しているときに行き着いた場所なので、共有したいと思いました。また、これはスタック オーバーフローに関する私の最初の投稿なので、もしそれが悪かったり何かあれば、批判していただきたいと思っています。:)
うーん、もしかしたらあなたの質問が正しく理解できていないかもしれません。
バックグラウンドワーカーは、自分の「workermethod」( 背景労働者.doWork-event) が完了するため、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
フラグを設定します。ただし、まだちょっとぎこちないです。cancel イベントを非同期メソッドのように扱い、現在実行していることをすべて実行することをお勧めします。 RunWorkerCompleted
ハンドラ。
ここのパーティーには少し遅れていますが (約 4 年)、UI をロックせずにビジー ループを処理できる非同期スレッドを設定し、そのスレッドからのコールバックで、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
キャンセルが成功した後に必要なメソッドを実行するには、擬似イベントのように機能します。これは UI スレッドから独立しているため、待機中に UI はロックされません。 MyWorker
キャンセルを終了します。本当にロックしてキャンセルを待つことが目的の場合、これは役に立ちませんが、別のプロセスを開始できるように待機したいだけの場合は、これはうまく機能します。
これは本当に遅い(5年)ことはわかっていますが、あなたが探しているのはスレッドと 同期コンテキスト. 。フレームワークに魔法のように自動的に実行させるのではなく、UI コールを UI スレッドに「手動」でマーシャリングする必要があります。
これにより、必要に応じて待機できるスレッドを使用できるようになります。
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
この問題に対する Fredrik Kalseth の解決策は、私がこれまでに見つけた中で最高のものです。他のソリューションでは、 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 クラス (マイクロソフトのドキュメント).
フォームを閉じると、開いているログファイルが閉じます。バックグラウンドワーカーがそのログファイルを書き込むので、 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
while ループ。
完全な実装の例を次に示します。
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();
この方法の問題点は次のとおりです。
- 必ず非同期メソッドを使用する必要があります。
- await Task.Delay は不正確です。私の PC では、Task.Delay(1) は実際には約 20 ミリ秒待機します。
ああ、これらのいくつかは途方もなく複雑になっています。必要なのは、DoWork ハンドラー内の BackgroundWorker.CancelPending プロパティを確認することだけです。いつでも確認できます。保留中になったら、e.Cancel = True に設定し、メソッドを終了します。
//ここでの方法Private void Worker_dowork(Object Sender、doworkeventargs e){backgroundworker bw =(backgroundworkerとしての送信者);
// do stuff
if(bw.CancellationPending)
{
e.Cancel = True;
return;
}
// do other stuff
}