10
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Taskを返すeventをawaitで待ってはいけない(気付きづらいうっかりミスの紹介)

Last updated at Posted at 2024-10-14

概要

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(); という記法があまりにも正しそうに見えるので、うっかり見逃してしまいがちです。気をつけましょう・・・。

10
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?