Unity非同期処理を理解しようとしてみた

# Unity非同期処理を理解しようとしてみた

Unityにおける非同期処理を理解するために、Simpleなコードから一歩づつ結果を見ていきます。

## 環境
* Windows10
* Unity 2019.2.11f1
* Visual Studio 2017

## 概要
1. Unityで重い処理を実行してみる
2. 重い処理を非同期実行してみる
3. 重い処理の結果を待ってみる
4. スレッドを追ってみる
5. 後処理を正しい位置に修正
6. (参考)Unity用のTaskパッケージ「UniTask」

## シーン構成

シーン上にはUpdateMethodで上下に移動するだけのCubeと重い処理が実行されるボタン、そのただ2つだけがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491627/0490209c-efd4-cfa8-24d9-17c6c30de7be.png)

## やりたいこと
* 重い処理をバックグラウンドで動かしつつ、Cubeの往復を止めない。
* 重い処理が終わったことを検知して、Cubeを消す。

## Unityで重い処理を実行してみる

ここからボタンの処理を書いていきます。
最もシンプルなコードから始めます。
ボタンを押すとHeavyMethodとFinalizeMethodが順番に実行されます。
HeavyMethodは1秒Sleepするだけの単純な処理ですが、このコードを実行するとどうなるでしょうか？

```C#:AsyncTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class AsyncTest : MonoBehaviour
{
    public void ButtonClick()
    {
        Debug.Log("Start Click Method");
        HeavyMethod();

        FinalizeMethod();
        Debug.Log("End Click Method");
    }

    void HeavyMethod()
    {
        Debug.Log("Start Heavy Method");
        Thread.Sleep(1000);
        Debug.Log("End Heavy Method");
    }

    void FinalizeMethod()
    {
        Debug.Log("Finalize Method");
    }
}
```

結果はこちらです。
そして**HeavyMethodの実行中は、Cubeの動きは止まります。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491627/ff8afed7-793c-c8dc-e747-0a18a1176cb2.png)

## 重い処理を非同期で実行してみる

やっぱり重い処理の実行中は、シーンの処理（この場合はCubeの動き）を止めて欲しくないわけです。
ということで、重い処理を非同期実行にしてみます。

```C#:AsyncTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class AsyncTest : MonoBehaviour
{
    public void ButtonClick()
    {
        Debug.Log("Start Click Method");
        Task.Run(HeavyMethod);

        FinalizeMethod();
        Debug.Log("End Click Method");
    }

    void HeavyMethod()
    {
        Debug.Log("Start Heavy Method");
        Thread.Sleep(1000);
        Debug.Log("End Heavy Method");
    }

    void FinalizeMethod()
    {
        Debug.Log("Finalize Method");
    }
}
```
`HeavyMethod();`を`Task.Run(HeavyMethod);`に書き換えただけです。
これで結果がこのようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491627/86dbed72-7a6c-a911-b399-a4b27711c170.png)

先ほどとは順番が変わっていますね。ButtonClickの中には

1. `HeavyMethod`
2. `FinilizeMethod`

となっているのに、順番が入れ替わって、

1. `FinilizeMethod`
2. `HeavyMethod`

の順番になっているわけです。

さらに言えば、Heavy Methodが終わる前に、Click Methodを抜けているのがわかります。
**この実装であれば、画面のCubeの動きも止まりません。**

## 重い処理の結果を待ってみる

さてここでまた新たな需要です。
「画面の動きは止めたくないけど、Finilize MethodはHeavy Methodが終わってから実行したい。」
わがままですね。
この需要を満たすように、処理を書き換えます。

```C#:AsyncTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class AsyncTest : MonoBehaviour
{
    public void ButtonClick()
    {
        Debug.Log("Start Click Method");
        Task.Run(HeavyMethod);

        // FinalizeMethod();
        Debug.Log("End Click Method");
    }

    void HeavyMethod()
    {
        Debug.Log("Start Heavy Method");
        Thread.Sleep(1000);
        Debug.Log("End Heavy Method");

        FinalizeMethod();
    }

    void FinalizeMethod()
    {
        Debug.Log("Finalize Method");
    }
}
```
`FinalizeMethod`の場所を移動しただけです。
実行結果はこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491627/3ebf7b86-9314-06b7-e972-d0a27f6baac3.png)

この方法もまた、**Cubeの移動は止まりません。**

でもちょっと待ってください。`FinalizeMethod`でもし、GameObjectの操作などをしたらどうでしょう？
試してみましょう。

```C#:AsyncTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class AsyncTest : MonoBehaviour
{
    public void ButtonClick()
    {
        Debug.Log("Start Click Method : " + Thread.CurrentThread.ManagedThreadId);
        Task.Run(HeavyMethod);

        // FinalizeMethod();
        Debug.Log("End Click Method : " + Thread.CurrentThread.ManagedThreadId);
    }

    void HeavyMethod()
    {
        Debug.Log("Start Heavy Method : " + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(1000);
        Debug.Log("End Heavy Method : " + Thread.CurrentThread.ManagedThreadId);

        FinalizeMethod();
    }

    void FinalizeMethod()
    {
        Debug.Log("Start Finalize Method : " + Thread.CurrentThread.ManagedThreadId);
        Destroy(GameObject.Find("Cube"));
        Debug.Log("Start Finalize Method : " + Thread.CurrentThread.ManagedThreadId);
    }
}
```

`FinalizeMethod`にCubeを破壊する処理と、おもむろにThreadのIdを出力する処理を追加しました。
結果はこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491627/2384627f-15e7-bdcc-e22b-434efeae1d9e.png)
**あれ？**
`FinalizeMethod`が途中で終了していますね。
Cubeも破壊されていないし、例外も宇宙の彼方に消えてしまいました。
`Task.Run()`で実行した`HeavyMethod`から以降の処理のThreadのIdが切り替わってますね。非同期にした処理が、マルチスレッドで動いているのがわかります。
Unityのシーン上のオブジェクトは、メインスレッド（番号1のThread）からしか、触れることができないので、このようになってしまいました。

## スレッドを追ってみる

1. Cubeが消えない
2. 例外が消える

という問題が発生していますが、一旦「Cubeが消えない」にフォーカスして、考えましょう。
`DestroyCube`をメインスレッドから実行できればいいわけです。
というわけで修正していきます。

```C#:AsyncTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class AsyncTest : MonoBehaviour
{
    private SynchronizationContext context;
    public void ButtonClick()
    {
        context = SynchronizationContext.Current;

        Debug.Log("Start Click Method : " + Thread.CurrentThread.ManagedThreadId);
        Task.Run(HeavyMethod);

        // FinalizeMethod();
        Debug.Log("End Click Method : " + Thread.CurrentThread.ManagedThreadId);
    }

    void HeavyMethod()
    {
        Debug.Log("Start Heavy Method : " + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(1000);
        Debug.Log("End Heavy Method : " + Thread.CurrentThread.ManagedThreadId);

        FinalizeMethod();
    }

    void FinalizeMethod()
    {
        Debug.Log("Start Finalize Method : " + Thread.CurrentThread.ManagedThreadId);

        context.Post(state => DestroyCube(), null);
        // Destroy(GameObject.Find("Cube"));
        Debug.Log("End Finalize Method : " + Thread.CurrentThread.ManagedThreadId);
    }

    void DestroyCube()
    {
        Debug.Log("DestroyCube : " + Thread.CurrentThread.ManagedThreadId);
        Destroy(GameObject.Find("Cube"));
    }
}
```

いきなり`context = SynchronizationContext.Current`というものが出てきました。
これは、異なるThread間で状態をやりとりするためのオブジェクトです。
この`context`を経由して、FinalizeMethod内で`context.Post()`とすることで、メインスレッドに処理を戻すことができます。
また、本筋とは関係ありませんが、Cubeの破壊とLogの出力をまとめたMethod`DestroyCube`を作成しました。

結果はこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491627/3d768e2a-f975-1168-fd15-dc8d1d869c0b.png)
ちゃんと`DestoryCube`がメインスレッドで実行され、シーン内のCubeが破壊されました。
ちなみにThreadIdが先程と異なっているのは、Unityが空いてるThreadを適当にPoolからPickUpして使うからです。自分で指定しているわけではありませんよ。

## 後処理を正しい位置に修正
さて、一応「やりたいこと」は実現できたように思いますね。
ただ、「重い処理の結果を待ってみる」でやったような、`FinalizeMethod()`を`HeavyMethod`内に書くのはよくない気がします。
`HeavyMethod`の本来の責務ではないし、構造がネストして複雑になります。
というわけで、`FinalizeMethod()`を本来あるべき`ButtonClick`内に戻します。

```C#:AsyncTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class AsyncTest : MonoBehaviour
{
    // private SynchronizationContext context;
    async void ButtonClick()
    {
        // context = SynchronizationContext.Current;

        Debug.Log("Start Click Method : " + Thread.CurrentThread.ManagedThreadId);
        await Task.Run(HeavyMethod);

        FinalizeMethod();
        Debug.Log("End Click Method : " + Thread.CurrentThread.ManagedThreadId);
    }

    void HeavyMethod()
    {
        Debug.Log("Start Heavy Method : " + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(1000);
        Debug.Log("End Heavy Method : " + Thread.CurrentThread.ManagedThreadId);

        // FinalizeMethod();
    }

    void FinalizeMethod()
    {
        Debug.Log("Start Finalize Method : " + Thread.CurrentThread.ManagedThreadId);

        // context.Post(state => DestroyCube(), null);
        DestroyCube();
        Debug.Log("End Finalize Method : " + Thread.CurrentThread.ManagedThreadId);
    }

    void DestroyCube()
    {
        Debug.Log("DestroyCube : " + Thread.CurrentThread.ManagedThreadId);
        Destroy(GameObject.Find("Cube"));
    }
}
```

ただ戻すだけでは、検証したように`Task.Run()`の結果をまたずに`FinalizeMethod()`が動いてしまいます。
今回は`Task.Run()`の前に`await`というものがありますね。これは、`Task.Run()`が完了するまで次の処理を待てよ、という意味です。
これを付けると、VisualStudio2017様に「呼び出し元に`async`をつけろよ」と言われます。言われた通りに`async`をつけました。

結果はこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/491627/80a37147-ad00-476f-a341-af4ba95b68f5.png)

いいですね。正しい順番で動いてます。先ほどと違うのは、*End Click Method*の位置でしょうか。
`Task.Run()`で実行した処理の中から、メインスレッドで処理をしたいという需要意外では、`SynchronizationContext`を使う必要はないってことなんですね。
これなら`DestroyCube`で発生した例外もcatchできます。

## Unity用のTask（UniTask）
今までは使ってきたTaskという仕組みは、C#に備わっている仕組みです。Unityの仕組みではありません。
これは`using System.Threading.Tasks;`をimportしていることからもわかりますね。
実は、UnityにはTaskをもっとUnityに特化させたUniTaskというものがあるようです。
これを使うと、処理が速くなるだけではなく後々いろいろメリットがありそうです。
合わせて、今回は自作メソッドごと`Task.Run()`で呼び出しましたが、今回言及しなかった、別スレッドでの処理の例外をメインスレッドで受けるためにも「UniTaskCompletionSource」などを使って直接awaitできるように修正したら、よりいいのではないでしょうか。

**使い方**
[UniTask](https://github.com/Cysharp/UniTask/releases)をGitHubから拾ってきて、UnityPackageをimportしたら、`using UniRx.Async;`を書く。

それでは。