もくじ
→https://qiita.com/tera1707/items/4fda73d86eded283ec4f
やりたいこと
ある自作ライブラリのメソッド中で、Taskをawaitをしているところがあったので、当然そのメソッドを.Wait()したらデッドロックするのだろうな、と思っていたら、いざやってみるとデッドロックしなかった。
「どんなTaskを.Wait()したらデッドロックするのか」を、MS公式の文書から見つけられなかったので、**なんでデッドロックしなかったのか?またデッドロック回避のための方法は?**をいろいろ試した&ほかの方の記事を見て調べた結果のメモ。
【お願い】
Taskに関する
「こういう書き方をするとデッドロックしますよ」
「こういう書き方をすると、デッドロック回避できますよ」の
Microft公式ドキュメントのありかをご存知の方、ぜひ教えてください。
参考
TaskをWaitしてはいけない
https://qiita.com/acple@github/items/8f63aacb13de9954c5da#task%E3%82%92wait%E3%81%97%E3%81%A6%E3%81%AF%E3%81%84%E3%81%91%E3%81%AA%E3%81%84
async/awaitと同時実行制御
https://ufcpp.wordpress.com/2012/11/12/asyncawait%E3%81%A8%E5%90%8C%E6%99%82%E5%AE%9F%E8%A1%8C%E5%88%B6%E5%BE%A1/
CA2007: Do not directly await a Task
https://docs.microsoft.com/ja-jp/visualstudio/code-quality/ca2007?view=vs-2019
ためしたこと
- Taskの中でawaitしているメソッドがあると、そのメソッドの処理が終わるまで、呼び出し元のスレッドに戻って処理を継続しようとする
- そのTask(名前をtaskとする)を呼ぶときに、task.Wait()とすると、呼んだスレッドを一旦停止して、taskの処理をしようとする
- で、呼んだスレッドは一旦停止しているのに、taskの中のawaitはそのスレッドに戻ろうとするので、デッドロックする
- デッドロックするパターンでも、awaitするときに**.configureAwait(false)**をつけてやると、戻り先のスレッドをええようにしてくれる(スレッドプールの空いているところに勝手に割り当ててくれる)
という、ざっくり知識はあったので、たぶんこの戻ろうとするスレッドが動いてるかどうか、がポイントなんだろうな、と当たりをつけて、そのあたりを調べるために下記のコードを書いて実験。
前提
- .netFramework 4.7.2
実験1 .ConfigureAwait(false)をつけているawaitがあるタスクを、UIスレッドから.Wait()で呼ぶ
- Taskを呼び出すのは、UIスレッド
- Taskの中のawaitするところに、.ConfigureAwait(false) をつける
- Taskを呼ぶときに.Wait()する
private async void Button_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click start");
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just before");
func1().Wait();
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just after");
}
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click end");
}
private async Task func1()
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.0");
await Task.Delay(2000).ConfigureAwait(false);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.1");
await Task.Delay(2000).ConfigureAwait(false);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.2");
await Task.Delay(2000).ConfigureAwait(false);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.3");
}
00:58:28 1: Button_Click start
00:58:28 1: func1 just before
00:58:28 1: func1 No.0
00:58:30 3: func1 No.1
00:58:32 3: func1 No.2
00:58:34 3: func1 No.3
00:58:34 1: func1 just after
00:58:34 1: Button_Click end
UIスレッドは、スレッドNo.1。
Taskの中も、await Task.Delayの前まではUIスレッドと同じスレッドNo.1でうごいている。
一度、await Task.Delayを行うと、そのあとからTaskを抜けるまで、スレッドNo.が変わる。(No.3)
Taskを抜けると、もとのUIスレッドNo.に戻ってくる。
(ただし、taskが処理をしている間、UIはフリーズした状態になっている!)
→ちゃんとしたアプリでは使えなさそう。
実験2 .ConfigureAwait(false)をつけていないawaitがあるタスクを、UIスレッドから.Wait()で呼ぶ
- Taskを呼び出すのは、UIスレッド
- Taskの中のawaitするところに、.ConfigureAwait(false) をつけない
- Taskを呼ぶときに.Wait()する
private async void Button_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click start");
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just before");
func1().Wait();
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just after");
}
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click end");
}
private async Task func1()
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.0");
await Task.Delay(2000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.1");
await Task.Delay(2000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.2");
await Task.Delay(2000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.3");
}
01:02:29 1: Button_Click start
01:02:29 1: func1 just before
01:02:29 1: func1 No.0
(ここでデッドロック)
Taskの中の、最初のawaitを行った時点で、デッドロックする。
awaitでUIスレッドに戻ろうとしたが、**func1().Wait();**でUIスレッドが待ちに入っているので戻ることができず、デッドロックしたと思われる。
実験3 .ConfigureAwait(false)をつけていないawaitがあるタスクを、別スレッドから.Wait()で呼ぶ
- Taskを呼び出すのは、UIスレッドではない別のスレッド
- Taskの中のawaitするところに、.ConfigureAwait(false) をつけない
- Taskを呼ぶときに.Wait()する
private async void Button_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click start");
var t = Task.Run(() =>
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just before");
func1().Wait();
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just after");
});
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click end");
}
private async Task func1()
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.0");
await Task.Delay(2000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.1");
await Task.Delay(2000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.2");
await Task.Delay(2000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.3");
}
}
01:08:15 1: Button_Click start
01:08:15 1: Button_Click end
01:08:15 3: func1 just before
01:08:15 3: func1 No.0
01:08:17 4: func1 No.1
01:08:19 4: func1 No.2
01:08:21 4: func1 No.3
01:08:21 3: func1 just after
- UIスレッドからtaskを呼んだ時にはデッドロックしていたのが、UIスレッドではない別のスレッドから同じ呼び方をすると、デッドロックしなくて済んだ。
- UIスレッドから呼んだtaskの中のawaitは、処理が終わるとUIスレッドに戻ろうとするが、別のスレッドから呼んだtaskの中のawaitは、処理が終わると空いているスレッドを使おうとするように見える。
- 結果、.Wait()で止めているスレッドに戻ろうとしないので、デッドロックしなくて済んでいる。
※たぶん、冒頭で書いていた、中でawaitしてるライブラリのメソッドなのに、そいつを.Wait()してもデッドロックしない奴、は、このパターンだったのでは?と思う。(まだ未確認)
実験4 .ConfigureAwait(false)をつけているawaitがあるタスクを、UIスレッドからawaitで呼ぶ
- Taskを呼び出すのは、UIスレッド
- Taskの中のawaitするところに、.ConfigureAwait(false) をつける
- Taskを呼ぶときにもawaitする
private async void Button_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click start");
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just before");
await func1();
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just after");
}
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click end");
}
private async Task func1()
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.0");
await Task.Delay(2000).ConfigureAwait(false);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.1");
await Task.Delay(2000).ConfigureAwait(false);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.2");
await Task.Delay(2000).ConfigureAwait(false);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.3");
}
01:19:22 1: Button_Click start
01:19:22 1: func1 just before
01:19:22 1: func1 No.0
01:19:24 3: func1 No.1
01:19:26 3: func1 No.2
01:19:28 3: func1 No.3
01:19:28 1: func1 just after
01:19:28 1: Button_Click end
taskの中で、一回awaitした後のスレッドNo.が、UIとは別のスレッドになる。
実験5 .ConfigureAwait(false)をつけていないawaitがあるタスクを、UIスレッドからawaitで呼ぶ
- Taskを呼び出すのは、UIスレッド
- Taskの中のawaitするところに、.ConfigureAwait(false) をつけない
- Taskを呼ぶときにもawaitする
private async void Button_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click start");
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just before");
await func1();
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just after");
}
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click end");
}
private async Task func1()
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.0");
await Task.Delay(2000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.1");
await Task.Delay(2000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.2");
await Task.Delay(2000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.3");
}
01:21:27 1: Button_Click start
01:21:27 1: func1 just before
01:21:27 1: func1 No.0
01:21:29 1: func1 No.1 // ←スレッドNo.が、実験4とことなる!!!
01:21:31 1: func1 No.2
01:21:33 1: func1 No.3
01:21:33 1: func1 just after
01:21:33 1: Button_Click end
実験4と、ログが出力されるタイミングはまったく同じだが、
taskの中で一回awaitした後の戻り先のスレッドNo.が異なる。
つまり、UIスレッドから呼ばれてawaitした処理はUIスレッドに戻ろうとする、ということだと思われる。
実験6 .ConfigureAwait(false)をつけていないawaitがあるタスクを、別スレッドからawaitで呼ぶ
- Taskを呼び出すのは、UIスレッドではない別のスレッド
- Taskの中のawaitするところに、.ConfigureAwait(false) をつけない
- Taskを呼ぶときにもawaitする
private async void Button_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click start");
var t = Task.Run(async () =>
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just before");
await func1();
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just after");
});
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click end");
}
private async Task func1()
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.0");
await Task.Delay(2000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.1");
await Task.Delay(2000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.2");
await Task.Delay(2000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.3");
}
01:28:12 1: Button_Click start
01:28:12 1: Button_Click end
01:28:12 3: func1 just before
01:28:12 3: func1 No.0
01:28:14 3: func1 No.1
01:28:16 3: func1 No.2
01:28:18 3: func1 No.3
01:28:18 3: func1 just after
実験7 .ConfigureAwait(false)をつけているawaitがあるタスクを、別スレッドからawaitで呼ぶ
- Taskを呼び出すのは、UIスレッドではない別のスレッド
- Taskの中のawaitするところに、.ConfigureAwait(false) をつける
- Taskを呼ぶときにもawaitする
private async void Button_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click start");
var t = Task.Run(async () =>
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just before");
await func1();
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 just after");
});
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: Button_Click end");
}
private async Task func1()
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.0");
await Task.Delay(2000).ConfigureAwait(false);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.1");
await Task.Delay(2000).ConfigureAwait(false);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.2");
await Task.Delay(2000).ConfigureAwait(false);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} {Thread.CurrentThread.ManagedThreadId}: func1 No.3");
}
01:30:41 1: Button_Click start
01:30:41 1: Button_Click end
01:30:41 4: func1 just before
01:30:41 4: func1 No.0
01:30:43 3: func1 No.1
01:30:45 3: func1 No.2
01:30:47 3: func1 No.3
01:30:47 3: func1 just after
追加でかきたいこと
・実験2のawaitするTaskの中に、awaitする処理が一つもないと、デッドロックしない
→最初に挙げた動きの原因は、間が抜けているが、これだった。
・async/awaitが階層になっていて、一番上で.Wait()する場合、
間に挟まるawaitするタスクは、一つでも.ConfigureAwait(false)が抜けていると、デッドロックする。