asyncを使った非同期メソッドの実装 の変更点
更新- 追加された行はこの色です。
- 削除された行はこの色です。
- プログラミング/C#/asyncを使った非同期メソッドの実装 へ行く。
- プログラミング/C#/asyncを使った非同期メソッドの実装 の差分を削除
[[公開メモ]] #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 待つ } // を繰り返す // ボタンが解除されたら stop を追加する 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(); } }); // 以下はサブスレッドから戻った後にメインスレッドの // コンテキストで実行されるため Invoke は必要ない listBox1.Items.Insert(0, "*** stop ***"); } ミソとなるのは、 + 内部で await するメソッドには async を付けておく + await Task.Run() によりサブスレッドを作成し実行 + async メソッドからはサブスレッドを起動した直後に呼び出し元に戻る + サブスレッドからメインスレッド上のコントロールにアクセスするためには Invoke を利用する + await から戻った後のコードはメインスレッドで実行されるので、 Invoke を使わずにコントロールにアクセス可能 というところになります。 5. が無ければ await Task.Run() の部分を new Thread(()=>{...}).Start() としても同じことなので、上記の説明はちょっと冗長なのですが、、、 GUI がらみの非同期処理を書くときの参考になれば、ということで。 * 以下はオフトピック [#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(()=>{...});" で済むようになります。 LANG:c# while ( Invoke(() => checkBox1.Checked) ) { // listBox1 にテキストを追加するのに Invoke を通す Invoke(() => { listBox1.Items.Insert(0, (i++).ToString()); }); Task.Delay(100).Wait(); } 各 Form に上記定義を加えるのが面倒と思えば、 LANG:c# public static class ControlInvokeExtensions { public static T Invoke<T>(this Control c, Func<T> func) { return (T)c.Invoke(func); } public static void Invoke(this Control c, Action action) { c.Invoke(action); } } のように拡張メソッドを定義することでメンバー関数を用意する必要がなくなるのですが、 これだと "this.Invoke(()=>...)" や "this.Invoke(()=>{...});" のように "this." を付けて呼ぶことになりますね。。。 ** チェックボックスをトグルボタンとして使う [#j69345a5] C# の Button にはトグル動作のためのプロパティがありません。 代わりに、 LANG:c# checkBox1.Appearance = System.Windows.Forms.Appearance.Button; checkBox1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; checkBox1.UseVisualStyleBackColor = true; とすることでトグル動作するボタンを作れるので、 こちらを使うことが推奨されているようです。 * コメント [#od9a35a8] #article_kcaptcha
Counter: 19802 (from 2010/06/03),
today: 1,
yesterday: 11