Doge log

Abby CTO 雑賀 力王のオフィシャルサイトです

Background Workerって???

今まで非同期デリゲートで書いていたんだが.NET2.0ではBackground Workerってのができたので試してみたが。

簡単なコード

    this.backgroundWorker1 = new BackgroundWorker();
    this.backgroundWorker1.WorkerSupportsCancellation = true;
    this.backgroundWorker1.DoWork += new System.ComponentModel.DoWorkEventHandler(this.backgroundWorker1_DoWork);
    this.backgroundWorker1.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.backgroundWorker1_RunWorkerCompleted);

と書いて

        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {
             //処理・・・・・
        }

        private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
             //完了した時の処理
        }

のように処理を書くわけだ。
実行する時は1行でいい。

            this.backgroundWorker1.RunWorkerAsync();

スレッドを意識しなくなった分(以前よりね)すっきりを書けます。
ただアホのように使うと問題が発生するケースがある。
アホは少ないと思うが参考までに問題になりそうなケースを挙げてみる。

処理を複数スレッドで実行したいケース

えーっと当たり前ですがBackground Workerを複数用意しないといけません。
(ホントはしなくてもいいんだけど)
単純にバンバン処理をBackground Workerに任せたいなあという場合、

            this.backgroundWorker1.RunWorkerAsync();

処理がCompleteしてないとこいつが"IsBusy"でこけます。
まあ当たり前ですけどね。
で"IsBusy"か評価して解除されるまで見てもよいですが非同期の意味ないですよね。

キャンセル地獄待ちケース

まーよくあるケースでバックグラウンドの処理をキャンセルしたいってケース。
キャンセルする場合は

            this.backgroundWorker1.CancelAsync();

と書けばよいだけ。
そうするとBackgroundWorker.CancellationPendingがtrueになってキャンセルされたことがキャッチできます。
(あくまでBackgroundWorker.CancellationPendingがtrueになるだけって所注意です。)
実行する処理(backgroundWorker1_DoWork)の中身が

            if (work.CancellationPending)
            {
                e.Cancel = true;
                return;
            }
            //ここにクソ長い処理がある
            Fucked.Fuck();

とかってなっているとタイミング次第では結局キャンセルしようにもできない。
Fuckメソッドがやばいくらい長い時間かかる場合、やりなおそうとしても

            this.backgroundWorker1.RunWorkerAsync();

処理がCompleteしてないのでこいつまた"IsBusy"でこけます。
まあ当たり前ですけどね。
でさらに

            if (work.CancellationPending)
            {
                e.Cancel = true;
                return;
            }
            //ここにクソ長い処理がある
            Fucked.Fuck();
            if (work.CancellationPending)
            {
                e.Cancel = true;
                return;
            }

このようにサンドイッチにしておかないと" backgroundWorker1_RunWorkerCompleted"メソッドで正常に行われたように振舞ってしまうので注意です。
(まあ当たり前か)
処理の流れとしてはキャンセルしたとはいえ"RunWorkerCompleted"を通ってきます。
"RunWorkerCompleted"は

  1. 処理が正常に終了した
  2. 処理がキャンセルされた
  3. 処理中に例外が発生した

といったとにかく処理が終わった時に呼ばれる仕様です。
注意しないと大分前に処理してキャンセルした結果が画面に出てきたりしてしまいますよ、奥さん。

回避するには

で回避方法としては

            if (this.backgroundWorker1.IsBusy)
            {
                this.backgroundWorker1.CancelAsync();
                this.backgroundWorker1 = new BackgroundWorker();
                this.backgroundWorker1.WorkerSupportsCancellation = true;
                this.backgroundWorker1.DoWork += new System.ComponentModel.DoWorkEventHandler(this.backgroundWorker1_DoWork);
                this.backgroundWorker1.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.backgroundWorker1_RunWorkerCompleted);
            }

なんて荒業やっちゃうってのも手かなと。
上書きされたbackgroundWorker1は?って話ですが。

実は裏でガンガン動いています。

基本的に捨てれません。裏で勝手に走らせておくしかないです。
非同期デリゲートの時代は最新のThreadかどうかを判別するためにticketを切ったりして最新の結果だけを有効化していました。
それに比べればスッキリした方かなと思います。
まー当たり前か。
重い処理を非同期にする場合は

  • 複数実行をサポートするのか?
  • キャンセルをサポートするのか?

をおさえ設計しましょう。
うくく。