Quantcast
Channel: プログラミング
Viewing all articles
Browse latest Browse all 8786

Taskを返すeventをawaitで待ってはいけない(気付きづらいうっかりミスの紹介) - 新しもの好きプログラマの耳より情報ブログ

$
0
0

概要

C#のeventは割と基本的な機能なので、気軽に使っていると思います。しかしawaitと組み合わせたら、意外なところで罠にはまったので、気をつけましょうという事で紹介です。

最初に結論まとめ

Taskを返すタイプのeventをawaitで待った場合、eventに登録されたデリゲート全ての完了を待たず、1つでも完了したら処理が進んでしまいます。これはおそらく、意図と違う動きになっていると思います。気をつけましょう。

良く考えてみれば当たり前の動きですが、見た目には次のような感じで普通に見えるので、コードをぱっと見ただけだとなかなか気付きませんでした。

event Func<Task> action1Async;

await action1Async();

詳しく紹介

Taskを返すタイプのeventとawaitを組み合わせた場合、一見、eventに登録されたデリゲート全ての完了を待機しているように見えます。しかし、実際には1つしか待機していない。という話です。

C#のeventは、Observerパターン的なものなので、複数のObserverを登録できます。複数の登録がある場合は、Invoke()を呼び出した時に、登録されたメソッドが同期で順番に呼ばれ、全ての処理が終わったら制御を返します。こんな感じです。

event Action action1;
action1 += () => Console.Write("1");
action1 += () => Console.Write("2");

//この状態でInvokeを呼び出すと
action1();
Console.Write("3");
//必ず"1"と"2"が出力されてから制御を返すので、コンソール出力は
//>123
//になる

※eventは本来はイベント登録とInvoke()を行える場所が異なりますが、本記事ではコードをシンプルにするため同じ場所から呼び出すコードを書きます

そのため、「Invoke()が制御を返したら、全ての登録済みデリゲートの処理は終わっている」と期待して使うと思います。

しかし、その感覚のままでawaitと組み合わせると、書き方によってはデリゲートの処理が終わる前に制御が返ってきてしまいます。こんな感じの場合です。

event Func<Task> action1Async;

async Task Func1()
{
    await Task.Delay(3000);
    Console.Write("1");
}
async Task Func2()
{
    Console.Write("2");
    await Task.CompletedTask;
}

action1Async += Func1;
action1Async += Func2;

//ここで、action1Asyncの完了をawaitしようとすると・・・
await action1Async();
Console.Write("3");
//"1"が出力されるより前に制御が返ってくるため、コンソール出力がこうなる
//>23

完了をawaitで待機しているはずなのに、なぜ完了前に制御が返ってきてしまうのでしょうか?

実は、よく考えてみると当然の動きだったりします。eventとawaitという便利な構文によって見えづらくなっているだけで、このコードはデリゲート1つ分の完了しか待機していません。ポイントは、eventをInvoke()した場合、全てのデリゲートの実行が終わった後に、「戻り値を1つだけ返す」という動作です。

このケースではFunc1とFunc2がasyncなので、「全てのデリゲートの実行が終わった」時点では、Taskを作成しただけであって処理は完了していません。返ってきたTask全ての完了を待つ必要があります。しかし、eventのInvokeは戻り値を1つしか返しません。つまりawait action1Async();の部分を分解すると、次のようなコードになっていることになります。

Task result = Func1();
result = Func2();
await result;

Func1が返したTaskは待機せずに放置されています。当然、そちらで例外が発生したとしてもcatchできません。

このように考えていくと当たり前なのですが、 await action1Async();という記法があまりにも正しそうに見えるので、うっかり見逃してしまいがちです。気をつけましょう・・・。


Viewing all articles
Browse latest Browse all 8786

Latest Images

Trending Articles