概要
タイトル通りうっかりミスでTaskの完了を待てていなかったという話なんですが、見た目に分かりづらくかなり恐ろしいなと思ったので、紹介します。皆さん同じ落とし穴に気をつけてください。
最初に結論まとめ
次のコード、じつはawait task;
でTaskの完了を待てていません。
async Task ThreadFunc(){
await Task.Delay(TimeSpan.FromSeconds(10));
}
var task = Task.Factory.StartNew(ThreadFunc);
await task;
完了を待つには、Unwrap()メソッドを入れて、こうする必要があります。
var task = Task.Factory.StartNew(ThreadFunc).Unwrap();
説明
次のコード、一見何の変哲も無いTask完了待ちのコードです。※1のスレッドが完了すると、Waitが終わって※2が実行されます。・・・という風に、なると思いますか?
async Task ThreadFunc(){
await Task.Delay(TimeSpan.FromSeconds(10));
//※1
}
var task = Task.Factory.StartNew(ThreadFunc);
await task;
//※2
これがぱっと見では気付きづらい罠にはまっていまして、※1のスレッド完了を待たずに、すぐ※2が実行されてしまいます。コーディングミスなのですが、型が上手いこと噛み合ってしまってコンパイルエラーにもなりません。
ThreadFuncがTask func()ではなく、次のようにvoid func()の場合は、問題ありません。
void ThreadFunc(){
Thread.Sleep(TimeSpan.FromSeconds(10));
}
var task = Task.Factory.StartNew(ThreadFunc);
await task;
何が悪いのか、見えてきたでしょうか?
コード上は全く同じに見える、Task.Factory.StartNewのオーバーロードが曲者です。
StartNewメソッドにFunc<Task>
を渡すと、戻り値にTask<Task>
が返ってきます。これは、「ThreadFuncがTaskを返してくるのを待つTask」です。(メソッド定義)
このtaskをawaitすると・・・本来待ちたかったTaskが返ってきた時点で、すぐに処理を抜けてしまいます。上のコードだと、await task;
で、実はTask task2 = await task;
のように戻り値が返ってきています。これを捨てているので、狙ったとおりに処理を待てずに先に抜けてしまうというわけです。
このような場合、Task<Task>
の中身をちゃんと待てるように、一工夫してやる必要があります。次のように、Unwrap()すればOKです。
async Task ThreadFunc(){
await Task.Delay(TimeSpan.FromSeconds(10));
//※1
}
var task = Task.Factory.StartNew(ThreadFunc).Unwrap();
await task;
//※2
このようにすると、await task;
が待つTaskは、ちゃんとThreadFuncが返してきたTaskとなります。これで解決です!
ちなみにこの問題、Task.Runメソッドを使う場合は起きません。このケースに対応するオーバーロードがあるようです。(メソッド定義)
TaskCreationOptionsを使いたい、スケジューラを分けたいなど、TaskFactory.StartNewを使う必要がある場合に注意すれば大丈夫です。
まとめ
ぱっと見では同じようなコードでTaskの完了を待っているはずなのに、オーバーロードの罠にはまって完了を待てない、というなかなか厄介なバグを紹介しました。知っていればメソッド呼び出しを1つ加えて解決ですが、コンパイルエラーにもならないし、なかなか気付きづらい罠だと思います。同じようなコードを書いた時は、罠にはまらないようにこの話を思い出してみてください。