async/awaitからC#のマルチスレッドについて調べた

## はじめに
Unityで`async/await`を利用した非同期処理を触り、色々と学びがあったため記載できればと思います。
内容は今後後付けすると思いますが、まずはこういう風に触ったらこういう挙動になるという書きやすいところから書いてみたいと思います。

## 実行環境
Unity2018.2.4

## C#でのマルチスレッド
導入として、以下のような実行に１秒間かかる重いメソッド`HeavyMethod`があると考えます。
（今回は`Thread.Sleep(1000)`で代用）

```c#:sample01.cs
using System.Threading;
using UnityEngine;
public class Test : MonoBehaviour
{
    void Start()
    {
        print("[Before] " + Time.realtimeSinceStartup);
        print(HeavyMethod()); // <= ここでアプリが1秒止まってしまう。
        print("[After] " + Time.realtimeSinceStartup);
    }

    string HeavyMethod()
    {
        Thread.Sleep(1000);
        return "Complete";
    }
}
```

この`HeavyMethod`関数をメインスレッドで実行すれば１秒間アプリが止まることになるため、ワーカースレッドでの実行を行う必要があります。
また、マルチスレッド化にあたって、以下の出力を順番を変えずに移植を行う必要があります。

> [Before] 1.620573
> Complete
> [After] 2.62794

これをC#で実現するためのいくつかの方法を書いてみます。

### Thread
直接スレッドを生成する場合です。

```c#:sample02.cs
using System.Collections;
using System.Threading;
using UnityEngine;
public class Test : MonoBehaviour
{
    IEnumerator Start()
    {
        print("[Before] " + Time.realtimeSinceStartup);
        string output = string.Empty;
        Thread t = new Thread(new ThreadStart(() =>
        {
            output = HeavyMethod();
        }));
        t.Start();
        while (t.IsAlive)
        {
            yield return null;
        }
        print(output);
        print("[After] " + Time.realtimeSinceStartup);
    }

    string HeavyMethod()
    {
        Thread.Sleep(1000);
        return "Complete";
    }
}
```
`new Thread()`を行って新しくスレッドを生成して、別スレッドで重い処理を実行しています。
スレッドの終了を`IsAlive`をメインスレッドでポーリングして検知するようにしています。
また、スレッドは値を返却できないため、メインスレッドで受け渡し用の変数`output`を作っています。

### ThreadPool
スレッドプールを利用する場合です。

```c#:sample03.cs
using System.Collections;
using System.Threading;
using UnityEngine;
public class Test : MonoBehaviour
{
    IEnumerator Start()
    {
        print("[Before] " + Time.realtimeSinceStartup);
        bool isComplete = false;
        string output = string.Empty;
        ThreadPool.QueueUserWorkItem(new WaitCallback((state) =>
        {
            output = HeavyMethod();
            isComplete = true;
        }));
        while (!isComplete)
        {
            yield return null;
        }
        print(output);
        print("[After] " + Time.realtimeSinceStartup);
    }

    string HeavyMethod()
    {
        Thread.Sleep(1000);
        return "Complete";
    }
}
```
`ThreadPool.QueueUserWorkItem()`でスレッドプールに重い処理を投げています。
スレッドと同じく、スレッドプールも値を返却できないため、メインスレッドで受け渡し用の変数`output`を作っています。
また、スレッドプールには終了を検知する仕組みもないため、`isComplete`というフラグを作成してメインスレッドでポーリングできるようにしています。

### delegate
デリゲートを利用する場合です。

```c#:sample04.cs
using System;
using System.Collections;
using System.Threading;
using UnityEngine;
public class Test : MonoBehaviour
{
    delegate string HeavyMethodDelegate();

    IEnumerator Start()
    {
        print("[Before] " + Time.realtimeSinceStartup);
        HeavyMethodDelegate worker = new HeavyMethodDelegate(HeavyMethod);
        IAsyncResult ar = worker.BeginInvoke(null, null);
        while (!ar.IsCompleted)
        {
            yield return null;
        }
        print(worker.EndInvoke(ar));
        print("[After] " + Time.realtimeSinceStartup);
    }

    string HeavyMethod()
    {
        Thread.Sleep(1000);
        return "Complete";
    }
}
```
`BeginInvoke()`でスレッドプールに重い処理を投げています。
`IAsyncResult`の`IsCompleted`をメインスレッドで終了をポーリングしています。
また、`EndInvoke()`でデリゲートを介して、スレッドから値を受け取っています。

### Task
Taskを利用した場合です。

```c#:sample05.cs
using System.Collections;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class Test : MonoBehaviour
{
    IEnumerator Start()
    {
        print("[Before] " + Time.realtimeSinceStartup);
        var task = Task.Run(() => HeavyMethod());
        while (!task.IsCompleted)
        {
            yield return null;
        }
        print(task.Result);
        print("[After] " + Time.realtimeSinceStartup);
    }

    string HeavyMethod()
    {
        Thread.Sleep(1000);
        return "Complete";
    }
}
```
`Task.Run`でスレッドプールに重い処理を投げています。
`IsCompleted`フラグを使ってメインスレッドで処理の完了を待機しています。
`Result`を使ってスレッドから値を受け取っています。

### Task + async/await
Taskとasync/awaitを組み合わせて利用した場合です。

```c#:sample06.cs
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class Test : MonoBehaviour
{
    async void Start()
    {
        print("[Before] " + Time.realtimeSinceStartup);
        print(await Task.Run(() => HeavyMethod()));
        print("[After] " + Time.realtimeSinceStartup);
    }

    string HeavyMethod()
    {
        Thread.Sleep(1000);
        return "Complete";
    }
}
```
`Task.Run`でスレッドプールに重い処理を投げています。
`async/await`構文を使ってメインスレッドで処理の完了待機と値の受け取りを行っています。
その他の書き方と違い、コルーチンを使ったポーリングの必要がありません。

## async/awaitの使い方
async/await構文の書き方は以下などに詳しく載っていますので、リンクだけ貼ります。

> Async および Await を使用した非同期プログラミング (C#)
> https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/concepts/async/

> async (C# リファレンス)
> https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/async

> await (C# リファレンス)
> https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/await

## async/awaitのスレッドの切り替わりについて
### Thread.CurrentThread.ManagedThreadId
async/awaitのスレッドの切り替わりをログを出力することで確認します。
今のコードがどのスレッドで実行されているかは`Thread.CurrentThread.ManagedThreadId`APIを使うことで知ることができます。

```c#:sample07.cs
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class Test : MonoBehaviour
{
    async void Start()
    {
        print("[1] " + Thread.CurrentThread.ManagedThreadId);
        await Task.Run(() => HeavyMethod());
        print("[4] " + Thread.CurrentThread.ManagedThreadId);
    }

    void HeavyMethod()
    {
        print("[2] " + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(1000);
        print("[3] " + Thread.CurrentThread.ManagedThreadId);
    }
}
```
> [1] ThreadID : 1
> [2] ThreadID : 245
> [3] ThreadID : 245
> <font color="Red">[4] ThreadID : 1</font>

上のように、`Task.Run`の内部に入ったときにThreadIDが`1`から`245`になり、スレッドの切り替えが行われていることが分かります。
その後、メインスレッドに戻ったときに再度`245`から`1`に戻っていることも分かります。

### ConfigureAwait
`Task`には`ConfigureAwait`というタスクが終了するときに、呼び出し元のスレッドに戻って後続の処理を行うか、現在のスレッドで後続の処理を行うかを選択できるAPIが用意されています。
こちらはデフォルトでは`True`になっており、呼び出し元のスレッドに戻るようになっています。
こちらを`False`にして実行してみたいと思います。

```c#:sample08.cs
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class Test : MonoBehaviour
{
    async void Start()
    {
        print("[1] ThreadID : " + Thread.CurrentThread.ManagedThreadId);
        await Task.Run(() => HeavyMethod()).ConfigureAwait(false);
        print("[4] ThreadID : " + Thread.CurrentThread.ManagedThreadId);
    }

    void HeavyMethod()
    {
        print("[2] ThreadID : " + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(1000);
        print("[3] ThreadID : " + Thread.CurrentThread.ManagedThreadId);
    }
}
```
> [1] ThreadID : 1
> [2] ThreadID : 255
> [3] ThreadID : 255
> <font color="Red">[4] ThreadID : 255</font>

呼び出し元の`Start`の[4]のログ出力がワーカースレッドで行われる挙動になっています。
ConfigureAwait(false)を設定するメリット、デメリットを述べたいと思います。

#### メリット
- パフォーマンスに優れています。

> .NETで非同期ライブラリを正しく実装する
> https://www.infoq.com/jp/articles/Async-API-Design
> あるデモでLucianはデフォルトのConfigureAwait(true)を使うと、ConfigureAwait(false)を使う場合より14倍遅いことを示しています。
> 1回の呼び出しの時間はわずかですが、ループの中で何千回も呼び出された場合、その時間が積み重なってしまいます。

- 呼び出し側で`Task.Wait()`を使われた場合でもアプリケーションがフリーズしません。

Taskの終了を待つAPIとして、`Task.Wait()`というタスクが完了するまで現在のスレッドを停止させるAPIが用意されています。
これを以下のコードのように叩いた場合、タスクのawait後に戻るスレッドが停止しているためにデッドロックが発生し、フリーズします。

```c#:sample09.cs
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class Test : MonoBehaviour
{
    void Start()
    {
        Task task = HeavyMethodAsync();
        task.Wait();
    }

    async Task HeavyMethodAsync()
    {
        // ここは呼び出し元のスレッド
        await Task.Run(() => Thread.Sleep(1000));
        // ここも呼び出し元のスレッド
    }
}
```
これを避けるためにはコードを以下のように修正します。

```c#:sample10.cs
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class Test : MonoBehaviour
{
    void Start()
    {
        Task task = HeavyMethodAsync();
        task.Wait();
    }

    async Task HeavyMethodAsync()
    {
        // ここは呼び出し元のスレッド
        await Task.Run(() => Thread.Sleep(1000)).ConfigureAwait(false);
        // ここはワーカースレッド
    }
}
```

#### デメリット
- ConfigureAwait(false)以下でUnityAPIを呼ぶことができません。

`sample10.cs`のポイントでスレッドIDを出力すると以下のようになります。

```c#:sample11.cs
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class Test : MonoBehaviour
{
    void Start()
    {
        print("[1] ThreadID : " + Thread.CurrentThread.ManagedThreadId);
        Task task = HeavyMethodAsync();
        task.Wait();
        print("[4] ThreadID : " + Thread.CurrentThread.ManagedThreadId);
    }

    async Task HeavyMethodAsync()
    {
        print("[2] ThreadID : " + Thread.CurrentThread.ManagedThreadId);
        await Task.Run(() => Thread.Sleep(1000)).ConfigureAwait(false);
        print("[3] ThreadID : " + Thread.CurrentThread.ManagedThreadId);
    }
}
```
> [1] ThreadID : 1
> [2] ThreadID : 1
> [3] ThreadID : 187
> [4] ThreadID : 1

[3]の部分では前述した通り、ワーカースレッドでプログラムは動作しています。
ここで以下のようにUnityAPIをコールしてみたときの出力は以下のようになります。

```c#:sample12.cs
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class Test : MonoBehaviour
{
    void Start()
    {
        print("[1] ThreadID : " + Thread.CurrentThread.ManagedThreadId);
        Task task = HeavyMethodAsync();
        task.Wait();
        print("[4] ThreadID : " + Thread.CurrentThread.ManagedThreadId);
    }

    async Task HeavyMethodAsync()
    {
        print("[2] ThreadID : " + Thread.CurrentThread.ManagedThreadId);
        await Task.Run(() => Thread.Sleep(1000)).ConfigureAwait(false);
        print("[3] ThreadID : " + Thread.CurrentThread.ManagedThreadId);
        gameObject.name = "HeavyMethodAsync"; // gameObjectにアクセス
    }
}
```
> [1] ThreadID : 1
> [2] ThreadID : 1
> [3] ThreadID : 200
> <font color="Red">get_gameObject can only be called from the main thread.</font>
> <font color="Red">UnityException: get_gameObject can only be called from the main thread.</font>

UnityAPIはメインスレッド以外で普通に使うと上のような例外が発生するので注意が必要です。

## UnityAPIをワーカースレッドで利用する
最後にUnityAPIをワーカースレッドで利用する方法を記載したいと思います。
以下のようなワーカースレッドでUnityAPIを叩くコードは前述した通り、エラーになります。

```c#:sample13.cs
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class Test : MonoBehaviour
{
    async void Start()
    {
        await Task.Run(() =>
        {
            Thread.Sleep(1000);
            gameObject.name = "Test"; // UnityException
        });
    }
}
```

このコードを以下のように修正します。

```c#:sample14.cs
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class Test : MonoBehaviour
{
    async void Start()
    {
        SynchronizationContext context = SynchronizationContext.Current;
        await Task.Run(() =>
        {
            context.Post(_ =>
            {
                gameObject.name = "Test";
            }, null);
            Thread.Sleep(1000);
        });
    }
}
```
`SynchronizationContext context = SynchronizationContext.Current;`をメインスレッドで行い、コンテキストを保持しておきます。
このコンテキストを使って、ワーカースレッドでメインスレッドに向けてUnityAPIをPostします。
この手順を踏むことでワーカースレッドのブロックの中でUnityAPIを呼ぶことができます。