await と Task.Result によるデッドロックによるとWindows FormでTaskを使ったときにデッドロックするケースがあるようです。 検証してみます。
デッドロックするソースコード
試しに次のようなコードを書いてみました。
namespace AwaitDeadLockEight { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { label1.Text = ""; string str = TimeCosumingMethod().Result; label1.Text = str; } private async Task<string> TimeCosumingMethod() { await Task.Delay(3000); return "TimeCosumingMethod の戻り値"; } } }
ボタンを押すと確かに固まります。
await Task.Delay(3000);
をコメントアウトすると動きます。
なので、非同期関数を呼ぶだけでは問題ないです。 非同期関数のなかで非同期関数を呼ぶと固まります。
Task.Delay(3000).Wait();
でも固まります。
await
を使うかどうかは関係ないようです。
デッドロックしないパターン
次のように呼び出すイベントハンドラーを非同期関数にすると期待通りに動きます。
private async void button1_Click(object sender, EventArgs e) { label1.Text = ""; string str = await TimeCosumingMethod(); label1.Text = str; } private async Task<string> TimeCosumingMethod() { await Task.Delay(3000); return "TimeCosumingMethod の戻り値"; }
不思議な挙動です。何が起きているのでしょうか?
今のところの仮説
ここからさきは、今のところの仮説です。 イマイチ上手く説明できていませんが、現在の理解を書いておきます。
- .NETにはSynchronizationContextというものがあり、非同期関数の処理結果をUIスレッドのコンテキストに戻している
- コンテキストを戻すとは、UIスレッドのイベントループのタスクキュー(?)にタスクを積んでいる
- スレッドは自タスクキューをLIFOで処理する
- 「Task.Delayの結果を処理する」タスクAがUIスレッドのタスクキューに積まれる
- 「TimeCosumingMethodの結果を処理する」タスクBがUIスレッドのタスクキューに積まれる
- LIFOでタスクBから処理しようとするが、タスクAが完了していないので、デッドロックする
でも、この説明だとイベントハンドラーを非同期関数にしたときに上手く動く理由が説明できません。
そもそも 「Task.Delayの結果を処理する」タスクAがUIスレッドのタスクキューに積まれる
は本当でしょうか?
ワーかスレッドのキューに積まれるないのでしょうか?
謎です。