10
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

[C#] .ConfigureAwait(false)のありなしで、使うスレッド/戻り先スレッドがどう変わるか実験

もくじ
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

どのスレッドで処理が実行されているのか?
https://qiita.com/rawr/items/5d49960a4e4d3823722f#%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%EF%BC%94%E3%81%A9%E3%81%AE%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89%E3%81%A7%E5%87%A6%E7%90%86%E3%81%8C%E5%AE%9F%E8%A1%8C%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%E3%81%AE%E3%81%8B

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

ためしたこと

  1. Taskの中でawaitしているメソッドがあると、そのメソッドの処理が終わるまで、呼び出し元のスレッドに戻って処理を継続しようとする
  2. そのTask(名前をtaskとする)を呼ぶときに、task.Wait()とすると、呼んだスレッドを一旦停止して、taskの処理をしようとする
  3. で、呼んだスレッドは一旦停止しているのに、taskの中のawaitはそのスレッドに戻ろうとするので、デッドロックする
  4. デッドロックするパターンでも、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");
        }
出力結果.txt
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");
        }
出力結果.txt
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");
        }
    }
出力結果.txt
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");
        }
出力結果.txt
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");
        }
出力結果.txt
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");
        }
出力結果.txt
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");
        }
出力結果.txt
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)が抜けていると、デッドロックする。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
10
Help us understand the problem. What are the problem?