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: 21310 (from 2010/06/03),
today: 1,
yesterday: 4