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

更新


公開メモ

Run ボタンを押している間動作を繰り返す

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

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

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 を使わずにコントロールにアクセス可能

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

以下はオフトピック

Invoke を呼ぶ呪文

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

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

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

チェックボックスをトグルボタンとして使う

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

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

コメント





*1 )=>...
*2 )=>{...}

Counter: 13968 (from 2010/06/03), today: 1, yesterday: 0