C#においてawait以降の処理はいつどこで実行されるのか

## はじめに

C#においてawait以降の処理は「どのスレッドで」「Taskが完了してからどのくらい遅延して」実行されるのか？
インターネット上の情報をもとに実験して得た、自分なりの理解をまとめてみる。
これはあくまで「このような挙動になったことを確認したもの」であり、言語仕様としてどう定義されているかではない。

※インターネット上の情報としてもっとも理解の助けになったページ：
[async/awaitと同時実行制御 | ++C++; // 未確認飛行 C ブログ](
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/)

## 実験手順

1. Visual Studio 2017で「Windows フォーム アプリケーション (.NET Framework)」プロジェクトを新規作成する
2. Form1にボタンコントロール「button1」とそのクリックイベントを追加する
3. Form1.csを以下のように実装する
4. 「デバッグの開始」をする
5. 「button1」をクリックする
6. デバッグ出力を確認する

``` Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            int startTick = System.Environment.TickCount;
            Log(startTick, "1", "UI", 0);
            A(startTick);
            Log(startTick, "6?", "UI", 500);
            System.Threading.Thread.Sleep(2000);
            Log(startTick, "14", "UI", 2500);
        }

        private async void A(int startTick)
        {
            Log(startTick, "2", "UI", 0);

            // await前にTaskが終わるケース（UIスレッドの場合）
            Task task = Task.Run(() => Sleep(startTick, "3", "4", 250, 0));
            System.Threading.Thread.Sleep(500);
            await task;
            Log(startTick, "5", "UI", 500);

            // await後にTaskが終わるケース（UIスレッドの場合）
            await Task.Run(() => B(startTick));
            Log(startTick, "15", "UIsync", 2500);
        }

        private async void B(int startTick)
        {
            Log(startTick, "7?", "work7?", 500);

            // await後にTaskが終わるケース（ワーカースレッドの場合）
            await Task.Run(() => Sleep(startTick, "8?", "9", 500, 500));
            Log(startTick, "10", "work8?>", 1000);

            // await前にTaskが終わるケース（ワーカースレッドの場合）
            Task task = Task.Run(() => Sleep(startTick, "11", "12", 250, 1000));
            System.Threading.Thread.Sleep(500);
            await task;
            Log(startTick, "13", "work8?>", 1500);
        }

        private void Sleep(int startTick, string seq1, string seq2, int sleep, int expectTaskStartTick)
        {
            string threadName = "work" + seq1;
            Log(startTick, seq1, "work" + seq1, expectTaskStartTick);
            System.Threading.Thread.Sleep(sleep);
            Log(startTick, seq2, "work" + seq1, expectTaskStartTick + sleep);
        }

        private void Log(int startTick, string seq, string threadName, int expectTick)
        {
            int threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
            int actualTick = System.Environment.TickCount - startTick;
            Console.WriteLine("({0,4}ms:{1,4}ms) [{2}:{3,-7}] {4}", expectTick, actualTick, threadId, threadName, seq);
        }
    }
}
```

### デバッグ出力のフォーマット

```
(想定経過時間ms:実測経過時間ms) [スレッドID:想定スレッド識別名] 想定到達順"
```

「想定スレッド識別名」は以下のような規則で命名している：

* UIスレッド（同期コンテキストを持つスレッド）で処理されるものは「UI」
* UIスレッドの同期コンテキストで処理されるものは「UIsync」
* ワーカースレッド（同期コンテキストを持たないスレッド）で処理されるものは「work」に「想定到達順」を付ける
* await以降の処理がTask側のスレッドで実行される場合、Task側の想定スレッド識別名に「>」を付ける

「想定到達順」については、非同期処理のタイミングによって順番が変わり得る部分には「?」を付けている。

## 実験結果

```
(   0ms:   0ms) [1:UI     ] 1
(   0ms:   0ms) [1:UI     ] 2
(   0ms:  31ms) [3:work3  ] 3
( 250ms: 281ms) [3:work3  ] 4
( 500ms: 531ms) [1:UI     ] 5
( 500ms: 531ms) [1:UI     ] 6?
( 500ms: 531ms) [3:work7? ] 7?
( 500ms: 531ms) [4:work8? ] 8?
(1000ms:1031ms) [4:work8? ] 9
(1000ms:1031ms) [4:work8?>] 10
(1000ms:1031ms) [3:work11 ] 11
(1250ms:1281ms) [3:work11 ] 12
(1500ms:1531ms) [4:work8?>] 13
(2500ms:2531ms) [1:UI     ] 14
(2500ms:2531ms) [1:UIsync ] 15
```

※実際には2と3の間にdllロードの出力があるが、ここでは関係ないので取り除いている

## 結果から読み取れる傾向

### await到達前にTaskが終わるケース

```
(   0ms:   0ms) [1:UI     ] 2
(   0ms:  31ms) [3:work3  ] 3
( 250ms: 281ms) [3:work3  ] 4
( 500ms: 531ms) [1:UI     ] 5
```

```
(1000ms:1031ms) [4:work8?>] 10
(1000ms:1031ms) [3:work11 ] 11
(1250ms:1281ms) [3:work11 ] 12
(1500ms:1531ms) [4:work8?>] 13
```

await前にTaskが終わっている場合はawaitに到達してもreturnせず、await以降の処理は「非同期」にも「後回し」にもされずに「そのままの流れで同期的に」実行される。

![task_first.png](https://qiita-image-store.s3.amazonaws.com/0/183757/0aab6b87-b851-136c-cf66-1649f1199f2e.png)

### await到達後にTaskが終わるケース

#### ワーカースレッドの場合

```
( 500ms: 531ms) [3:work7? ] 7?
( 500ms: 531ms) [4:work8? ] 8?
(1000ms:1031ms) [4:work8? ] 9
(1000ms:1031ms) [4:work8?>] 10
```

ワーカースレッドで未完了のTaskをawaitした場合、awaitに到達したスレッドはそこで呼び出し元にreturnし、await以降の処理は「Task側のスレッドでTask側の処理を完了したあと即座に」実行される。

![await_first.png](https://qiita-image-store.s3.amazonaws.com/0/183757/a54593bc-0b20-44ec-ec0f-140f79b688f1.png)

総じて（await前にTaskが終わるケースも含め）、ワーカースレッドでawaitした場合はawait以降の処理は「びりっけつで処理を終えたスレッドで実行される」、そんなイメージ。
自分の担当分を先に終えたワーカーは先に帰り（そして次の仕事を始め）、最後まで仕事をしていたワーカーが責任を持って後始末をするのである。

#### UIスレッドの場合

```
( 500ms: 531ms) [1:UI     ] 5
( 500ms: 531ms) [1:UI     ] 6?
(1500ms:1531ms) [4:work8?>] 13
(2500ms:2531ms) [1:UI     ] 14
(2500ms:2531ms) [1:UIsync ] 15
```

UIスレッドで未完了のTaskをawaitした場合、awaitに到達したスレッドはそこで呼び出し元にreturnし、await以降の処理は「Task完了時に高優先度のメッセージとしてUIスレッドのメッセージキューに追加される」、そんなイメージ。

![ui_await.png](https://qiita-image-store.s3.amazonaws.com/0/183757/0db8f2ef-1805-06c8-24f5-1221e5f7161a.png)