asyncを使った非同期メソッドの実装 のバックアップソース(No.1)

更新

[[公開メモ]]

#contents

* Run ボタンを押している間動作を繰り返す [#x86ee113]

フォームにボタンスタイルのチェックボックスと、結果表示用のリストボックスを配置し、
ボタンを押すと、もう一度押して解除するまでリストボックスにテキストを
追加し続けるアプリケーションを作ってみます。

次のような安易なコードを書いてしまうと、
リストボックスにテキストを表示し続けている間に
ボタンの押下を検出できないため、
アプリケーションがデッドロックしてしまいます。

 LANG:c#
 // これはうまく動かない
 private void checkBox1_CheckedChanged(object sender, EventArgs e)
 {
     if (!checkBox1.Checked)
         return;
 
     var i = 0;
     while (checkBox1.Checked) {                     // ボタンが解除されるまで
         listBox1.Items.Insert(0, (i++).ToString()); // 1行追加して
         Task.Delay(100).Wait();                     // 100ms 待つ
     }
     listBox1.Items.Insert(0, "*** stop ***");
 }

昔ながらの方法であれば while 内で Application.DoEvents() 
を呼んでお茶を濁したり、

- ボタンが押されたらスレッドを起動
- サブのスレッド内からは listBox にアクセスできないためWindowsメッセージでやりとりする
- もう一度ボタンが押されたらスレッドを止める

などとしそうなところですが、async, await, Invoke などを使うことで
上記の安易なコードそっくり(?)のコードを問題なく動かせました。

 LANG:c#
 private async void checkBox1_CheckedChanged(object sender, EventArgs e)
 {
     if (!checkBox1.Checked)
         return;
 
     await Task.Run(()=>{  // スレッドを起動し、終了を待たずに戻る
         var i = 0;
         // checkBox1.Checked を確認するのに Invoke を通す
         while ((bool)Invoke(new Func<bool>(() => checkBox1.Checked))) {
             // listBox1 にテキストを追加するのに Invoke を通す
             Invoke(new Action(() => {
                 listBox1.Items.Insert(0, (i++).ToString());
             }));
             Task.Delay(100).Wait();
         }
     });
     listBox1.Items.Insert(0, "*** stop ***");
 }

ミソとなるのは、
- 内部で await するメソッドには async を付けておく
- await Task.Run() によりサブスレッドを作成し実行
- async メソッドからはサブスレッドを起動した直後に呼び出し元に戻る
- サブスレッドからメインスレッド上のコントロールにアクセスするためには Invoke を利用する
- await から戻った後のコードはメインスレッドで実行されるので、
Invoke を使わずにコントロールにアクセス可能

というところになります。

* 以下はオフトピック [#r300fd10]

** Invoke を呼ぶ呪文 [#i08157d7]

"(bool)Invoke(new Func<bool>(()=>...))" や "Invoke(new Action(()=>{...}));" 
が冗長と思う場合には、Form の内部で

 LANG:c#
 T Invoke<T>(Func<T> func) { return (T)base.Invoke(func); }
 void Invoke(Action action) { base.Invoke(action); }

としておけば、それぞれ 
"Invoke(()=>...)" や "Invoke(()=>{...});" で済むようです。

** チェックボックスをトグルボタンとして使う [#j69345a5]

 LANG:c#
 checkBox1.Appearance = System.Windows.Forms.Appearance.Button;
 checkBox1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
 checkBox1.UseVisualStyleBackColor = true;

とすることでトグル動作するボタンを作れます。

* コメント [#od9a35a8]

#article_kcaptcha


Counter: 17598 (from 2010/06/03), today: 4, yesterday: 0