web-dev-qa-db-ja.com

BackgroundWorkerがキャンセルされるのを待つ方法は?

あなたのために何かをするオブジェクトのhypotheticalメソッドを考えてみましょう:

_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は再入不可です)。

私は、イベントのコードwaits-ワーカーのRunWorkerCompletedイベントハンドラーが設定するイベント同期オブジェクトを含むソリューションを使用したいと考えています。何かのようなもの:

_Event _workerDoneEvent = new WaitHandle();

public void CancelDoingStuff()
{
    _worker.CancelAsync();
    _workerDoneEvent.WaitOne();
}

private void RunWorkerCompletedEventHandler(sender object, RunWorkerCompletedEventArgs e)
{
    _workerDoneEvent.SetEvent();
}
_

しかし、私はデッドロックに戻りました。アプリケーションがアイドル状態になるまでイベントハンドラーは実行できず、アプリケーションはイベントを待機しているためアイドル状態になりません。

では、BackgroundWorkerが終了するのをどのように待つことができますか?


Update人々はこの質問に混乱しているようです。彼らは私がBackgroundWorkerを次のように使用すると考えているようです:

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

それはnotそれ、つまりnot私がやっていること、それはnotここで尋ねられていること。その場合、バックグラウンドワーカーを使用しても意味がありません。

117
Ian Boyd

私があなたの要件を正しく理解していれば、次のようなことができます(コードはテストされていませんが、一般的な考え方を示しています):

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
}
122
Fredrik Kalseth

this 応答に問題があります。 UIは、待機中にメッセージを処理し続ける必要があります。そうしないと、バックグラウンドワーカーがキャンセルリクエストに応答するのに時間がかかる場合に問題になります。

2番目の欠点は、ワーカースレッドが例外をスローした場合にメインスレッドが無期限に待機している場合、_resetEvent.Set()が呼び出されないことですが、この欠陥はtry/finallyブロックで簡単に修正できます。

これを行う1つの方法は、モーダルダイアログを表示することです。モーダルダイアログには、バックグラウンドワーカーが作業を終了した(または、キャンセルした場合)かどうかを繰り返しチェックするタイマーがあります。バックグラウンドワーカーが終了すると、モーダルダイアログはアプリケーションに制御を返します。これが発生するまで、ユーザーはUIを操作できません。

別の方法(最大1つのモードレスウィンドウが開いていると仮定)は、ActiveForm.Enabled = falseを設定し、バックグラウンドワーカーがキャンセルを完了するまでApplication、DoEventsでループし、その後ActiveForm.Enabled = trueを再度設定できます。

13
Joe

ほとんどすべての人が質問に混乱し、労働者がどのように使用されているのか理解していない。

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

今では、そのようなコードを書くことができますが、私はそうするつもりはありません。気にしない、気にしない。

10
Ian Boyd

RunWorkerCompletedEventHandlerRunWorkerCompletedEventArgsをチェックして、ステータスを確認できます。成功、キャンセル、またはエラー。

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");
}
4
Seb Nilsson

do n'tバックグラウンドワーカーの完了を待ちます。これは、別のスレッドを起動する目的をほとんど無効にします。代わりに、メソッドを終了させ、完了に依存するコードを別の場所に移動する必要があります。ワーカーに完了したことを知らせ、残りのコードを呼び出します。

waitで何かを完了するには、WaitHandleを提供する別のスレッド構造を使用します。

4
Joel Coehoorn

BackgroundWorker.RunWorkerCompletedイベントに結び付けられないのはなぜですか。これは、「バックグラウンド操作が完了したとき、キャンセルされたとき、または例外が発生したときに発生する」コールバックです。

3
Rick Minerich

BackgroundWorkerの完了を待つ理由がわかりません。クラスの動機付けとは正反対のように思えます。

ただし、worker.IsBusyを呼び出してすべてのメソッドを開始し、実行中の場合は終了させることができます。

1
Austin Salonen

ループ内で非同期プロセスを実行している間、バックグラウンドワーカーが待機する必要があるため、ここに来たと言いたいだけです。

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

解決策を探している間にここが終わったので、共有したいと思いました。また、これはスタックオーバーフローに関する私の最初の投稿です。 :)

1
Connor Williams

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

try ... catchwhileDoWorkループをカプセル化して、例外で_isBusy is falseを設定することもできます。または、StopAsync whileループで_worker.IsBusyをチェックするだけです。

完全な実装の例を次に示します。

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. task.Delayは不正確です。私のPCでは、Task.Delay(1)は実際に20msまで待機します。
0
Anthony

たぶん私はあなたの質問を正しくしていない。

バックグラウンドワーカーは、 'workermethod'( backgroundworker.doWork-event を処理するメソッド/関数/サブ)が終了するとWorkerCompletedイベントを呼び出すため、BWがまだ実行中かどうかを確認する必要はありません。ワーカーを停止する場合は、「ワーカーメソッド」内の キャンセル保留プロパティ を確認してください。

0
Stephan

この問題に対する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は、DoWorkハンドラーで例外が発生した場合でも、_resetEvent.Set();が呼び出されるようにします。そうしないと、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.)
0
A876

私はこれが本当に遅い(5年)ことを知っていますが、あなたが探しているのはスレッドと SynchronizationContext を使用することです。フレームワークに自動的に行わせるのではなく、「手動で」UIスレッドにUI呼び出しをマーシャリングする必要があります。

これにより、必要に応じて待機できるスレッドを使用できます。

0
Tom Padilla
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
0
Nitesh

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;
    }
}
0
Shawn Rubie

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

本当にメソッドを終了させたくない場合は、派生したAutoResetEventBackgroundWorkerのようなフラグを付けてから、OnRunWorkerCompletedフラグを設定します。しかし、それでもまだ気味が悪いです。 cancelイベントを非同期メソッドのように扱い、RunWorkerCompletedハンドラーで現在実行していることをすべて実行することをお勧めします。

0
OwenP

私はここでパーティーに少し遅れています(約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スレッドとは別であるため、MyWorkerがキャンセルを完了するのを待つ間、UIをロックしません。本当にロックしてキャンセルを待つことを意図している場合、これは役に立たないが、別のプロセスを開始できるように単に待ちたい場合、これはうまく機能する。

0
Infotekka