@ledsun blog

無味の味は佳境に入らざればすなわち知れず

Windows Formの同期イベントハンドラーから入れ子になった非同期関数を呼ぶとデッドロックする

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 の戻り値";
}

不思議な挙動です。何が起きているのでしょうか?

今のところの仮説

ここからさきは、今のところの仮説です。 イマイチ上手く説明できていませんが、現在の理解を書いておきます。

  1. .NETにはSynchronizationContextというものがあり、非同期関数の処理結果をUIスレッドのコンテキストに戻している
  2. コンテキストを戻すとは、UIスレッドのイベントループのタスクキュー(?)にタスクを積んでいる
  3. スレッドは自タスクキューをLIFOで処理する
  4. 「Task.Delayの結果を処理する」タスクAがUIスレッドのタスクキューに積まれる
  5. 「TimeCosumingMethodの結果を処理する」タスクBがUIスレッドのタスクキューに積まれる
  6. LIFOでタスクBから処理しようとするが、タスクAが完了していないので、デッドロックする

でも、この説明だとイベントハンドラーを非同期関数にしたときに上手く動く理由が説明できません。 そもそも 「Task.Delayの結果を処理する」タスクAがUIスレッドのタスクキューに積まれる は本当でしょうか? ワーかスレッドのキューに積まれるないのでしょうか? 謎です。

参考