[C#]async/awaitとUniRx.Asyncの実装を追う

ついに`Unity`でも`Incremental Compiler`を有効にすることで`async/await`が使用することができるようになりました。
これは大変うれしいことではありますが、`async/await`が何をやっているかわからないと実行速度や効率の面で不安になるかもしれません。
そこで今回は`async/await`が裏で何をやっているのか、`async/await`を`Unity`で便利に使用することができる`UniRx.Async`が裏で何をやっているのかについて解説していきます。
`async/await`の使い方や詳細についてはすでに色々記事があるとは思うのでそちらも参考にしてください。

## async/awaitの実装について
### asyc/awaitのおさらい

`async/await`を使用すると以下のような書き方ができます。

```csharp
async Task Hoge()
{
    Console.WriteLine("1");
    await Task.Delay(1000);
    Console.WriteLine("2");
}
```

上記のプログラムを実行すると`1`と表示された1秒後に`2`と表示されます。
このように`async/await`を書くと非同期処理をあたかも同期処理のように書くことができます。

### awaitでは何が起こるのか

`C#`において`await`はコンパイラでのコード生成によって実現されており、特殊なランタイムの機能を使用しているわけではありません。
これは`await`キーワードを使用しなくても自分の手で`await`相当のことを記述できることを意味しています。
(厳密には無理ですが、それっぽいことができるということです。また、下記のものはコンパイラによって生成されるものとは内容が異なりますが同じような意味になります。)

`await`を自分の手で記述するためには関数を`await`以前、`await`以降に分ける必要があります。
最初の例でいうと以下のような感じです。

```csharp
void Hoge()
{
    Console.WriteLine("1");
    // await部分
}

void HogeContinuation()
{
    Console.WriteLine("2");
}
```

次に`await`部分です。
`await`はタスクから`Awaiter`を取り出し、`Awaiter`に対して後続の処理を依頼します。
`Awaiter`は自身のタスクが完了したときに渡された後続の処理を実行します。

```csharp
void Hoge()
{
    Console.WriteLine("1");

    // await を展開した部分
    var task = Task.Delay(1000);
    // Awaiterを取り出す
    var awaiter = task.GetAwaiter();
    // 後続部分
    Action continuation = () =>
    {
        // awiterの結果を受け取る
        // タスクの中で例外が発生していた場合などはGetResultを呼んだスレッドで改めて例外が投げられる
        awaiter.GetResult();
        // 本来の後続部分を実行
        HogeContinuation();
    };
    // タスクがすでに完了している場合はそのスレッド上で後続を実行
    if (awaiter.IsCompleted)
    {
        continuation();
    }
    // 完了していない場合はAwaiterに後続部分の処理を任せる
    else
    {
        awaiter.OnCompleted(continuation);
    }
}
```

`await`で戻り値を受け取る場合もほぼ同じです。
違うのは`Awaiter.GetResult`の戻り値を後続に渡すようになっているくらいです。

### asyncでは何が起こるのか

`async`も`await`同様、コンパイラでのコード生成によって実現されており、特殊なランタイムの機能を使用しているわけではありません。
例によって`async`と同様の処理を自分の手で書くことができます。
`async`はすべての処理が終わった時に完了になるタスクを返す関数を作成します。
分かりにくいかもしれませんが以下のようになります。

```csharp
Task Hoge()
{
    var completionSource = new TaskCompletionSource<bool>();
    Console.WriteLine("1");

    var task = Task.Delay(1000);
    var awaiter = task.GetAwaiter();
    Action continuation = () =>
    {
        awaiter.GetResult();
        HogeContinuation();
        // 後続処理がすべて終わったことを通知する
        // もしcompletionSource.Taskが誰かにawaitされている場合(=AwaiterのOnCompletedが呼ばれている場合)、
        // この関数の中でawaitの続きの処理が実行される
        completionSource.SetResult(true);
    };
    if (awaiter.IsCompleted)
    {
        continuation();
    }
    else
    {
        awaiter.OnCompleted(continuation);
    }
    
    // 後続処理がすべて終わった後に完了するタスクを返す
    return completionSource.Task;
}
```

### task-likeについて

`async/await`で使用する`Task`クラスは大変便利なのですが様々な問題を抱えています。
それは`Task`クラスがとても複雑であること、クラスなので必ずアロケーションが発生してしまうことなどです。
普通のアプリケーションであれば絶対にどうにかしなければならないレベルではないと思いますが、
`Unity`においては使うのをためらってしまうレベルです。(個人差があります。)
この問題を解消するのが`task-like`です。
`task-like`は`Task`クラスっぽくふるまえる型のことを指し、`task-like`であれば`async`メソッドの戻り値として使用できます。
`task-like`型を自分で実装することで`async/await`を使用しつつパフォーマンスにも配慮することができます。
`UniRx.Async`はこの`task-like`型の実装とそれらについての便利メソッドを提供しています。

## UniRx.Asyncについて
### UniTask
`UniRx.Async`で一番基礎となる`task-like`な型です。
`async`メソッドを使うときはこの`UniTask`を戻り値にすることで`Task`を戻り値にするよりパフォーマンスに優れています。
ソースを見ればわかりますが、この型自体はとてもシンプルな作りになっています。
参考:[UniRx/UniTask.cs at master · neuecc/UniR](https://github.com/neuecc/UniRx/blob/master/Assets/Plugins/UniRx/Scripts/Async/UniTask.cs)


### UniTaskをawaitしたときに何が起こるのか

```csharp
async UniTask Hoge()
{
    var token = this.GetCancellationTokenOnDestroy();
    Debug.Log(1);
    // 1フレーム待つ
    await UniTask.Yield(PlayerLoopTiming.Update, token);
    Debug.Log(2);
}
```

上記の処理で`await`している部分で何が起きているかを見ていきます。

前述の通り、`await`は`Awaiter`を取得して`OnCompleted`を呼んでいました。
`UniTask`の`GetAwaiter`を見てみましょう。

```csharp
// UniTask.csより引用
[DebuggerHidden]
public Awaiter GetAwaiter()
{
    return new Awaiter(this);
}
```

`Awaiter`を作って返しているだけなので、`Awaiter`の`OnCompleted`を見てみましょう。

```csharp
// UniTask.csより引用
[DebuggerHidden]
public void OnCompleted(Action continuation)
{
    if (task.awaiter != null)
    {
        task.awaiter.OnCompleted(continuation);
    }
    else
    {
        continuation();
    }
}
```

`task.awaiter`の`OnCompleted`を呼んでいるようです。
少し調べると`task.awaiter`は`UniTask`のコンストラクタで渡している`IAwaiter`型のインスタンスであることがわかります。

```csharp
// UniTask.csより引用
readonly IAwaiter awaiter;

[DebuggerHidden]
public UniTask(IAwaiter awaiter)
{
    this.awaiter = awaiter;
}
```

よって、`UniTask`を`await`したときの処理は`UniTask`本体ではなくそれを作る際に使用した`IAwaiter`にあるといえます。
`UniTask.Yield`の場合を見てみます。

```csharp
// UniTask.Delay.csより引用
public static UniTask Yield(PlayerLoopTiming timing, CancellationToken cancellationToken)
{
    return new UniTask(new YieldPromise(timing, cancellationToken));
}
```

`YieldPromise`が本体っぽいです。

```csharp
// UniTask.Delay.csより引用
class YieldPromise : PlayerLoopReusablePromiseBase
{
    public YieldPromise(PlayerLoopTiming timing, CancellationToken cancellationToken)
        : base(timing, cancellationToken, 2)
    {
    }

    protected override void OnRunningStart()
    {
    }

    public override bool MoveNext()
    {
        Complete();
        if (cancellationToken.IsCancellationRequested)
        {
            TrySetCanceled();
        }
        else
        {
            TrySetResult();
        }

        return false;
    }
}
```

このクラスには`OnCompleted`がないので継承先のクラスを探します。
すると`ReusablePromise`に`OnCompleted`が見つかりました。

```csharp
// ReusablePromise.csより引用
public void OnCompleted(Action action)
{
    UnsafeOnCompleted(action);
}

public void UnsafeOnCompleted(Action action)
{
    if (continuation == null)
    {
        continuation = action;
        return;
    }
    else
    {
        if (continuation is Action act)
        {
            var q = new MinimumQueue<Action>(4);
            q.Enqueue(act);
            q.Enqueue(action);
            continuation = q;
            return;
        }
        else
        {
            ((MinimumQueue<Action>)continuation).Enqueue(action);
        }
    }
}
```

色々やっていますが、結局は渡されたアクションを内部に保持しているだけです。
なので`OnCompleted`を呼んだだけでは何も起こらないことが分かります。

では`await`をしても何も起こらないかというとそうではありません。
`await`をしたときに、`OnCompleted`以外にも`IsCompleted`が呼ばれているのでした。
`IsCompleted`の中を見てみましょう。
`IsCompleted`は`PlayerLoopReusablePromiseBase`の中にあります。

```csharp
// ReusablePromise.csより引用
public override bool IsCompleted
{
    get
    {
        if (Status == AwaiterStatus.Canceled || Status == AwaiterStatus.Faulted) return true;

        if (!isRunning)
        {
            isRunning = true;
            ResetStatus(false);
            OnRunningStart();
#if UNITY_EDITOR
            TaskTracker.TrackActiveTask(this, capturedStackTraceForDebugging);
#endif
            PlayerLoopHelper.AddAction(timing, this);
        }
        return false;
    }
}
```

重要なのは`PlayerLoopHelper.AddAction`です。
詳しくは書きませんがこれは`Unity`の`PlayerLoopSystem`を使って所定のタイミングで所定のメソッドを呼んでもらう機能です。
これによって次のフレームの`Update`のタイミングで`YieldPromise`の`MoveNext`が呼ばれるようになります。
`YieldPromise`の`MoveNext`を再掲します。

```csharp
// UniTask.Delay.csより引用
public override bool MoveNext()
{
    Complete();
    if (cancellationToken.IsCancellationRequested)
    {
        TrySetCanceled();
    }
    else
    {
        TrySetResult();
    }

    return false;
}
```

おそらく`TrySetResult`の中で後続の処理が呼ばれているのだろうと予想がつきます。
`ReusablePromise`を見てみます。

```csharp
// ReusablePromise.csより引用
public virtual bool TrySetResult()
{
    if (status == AwaiterStatus.Pending)
    {
        status = AwaiterStatus.Succeeded;
        TryInvokeContinuation();
        return true;
    }
    return false;
}

void TryInvokeContinuation()
{
    if (continuation == null) return;

    if (continuation is Action act)
    {
        continuation = null;
        act();
    }
    else
    {
        // reuse Queue(don't null clear)
        var q = (MinimumQueue<Action>)continuation;
        var size = q.Count;
        for (int i = 0; i < size; i++)
        {
            q.Dequeue().Invoke();
        }
    }
}
```

予想通り、`TrySetResult`を呼ぶと最終的に継続処理が呼ばれているようです。
まとめると以下のようになります。

`await`を実行したフレームでは`PlayerLoopHelper`を使用して処理を登録する。
次のフレームで`PlayerLoopHelper`に登録した処理が実行される。
これによってタスクが完了扱いになりそのスレッド上で後続処理が呼ばれる。

今回は`Yield`を見ましたが`Delay`なども大体同じです。
`UIButton`の`OnClickAsync`の用に`Unity`のイベントなどが元になっているものについては`PlayerLoopHelper`を使用せずに、イベント登録などが使用されます。

注意点として`await`をすると`PlayerLoop`に登録されるということです。
`PlayerLoop`に登録されてしまうと、`await`を行ったときの`GameObject`が`Destroy`されても後続処理が実行されてしまいます。

```csharp
void Start()
{
    Hoge().Forget();
    Destroy(this.gameObject);    
}

async UniTask Hoge()
{
    Debug.Log(1);
    await UniTask.Delay(100);
    Debug.Log(2);
}
```

上記の処理を実行したとき、`Debug.Log(2)`も実行されてしまいます。
回避するためには必ず`CancellationToken`を渡しましょう。

```csharp
void Start()
{
    var token = this.GetCancellationTokenOnDestroy();
    Hoge(token).Forget();
    Destroy(this.gameObject);    
}

async UniTask Hoge(CancellationToken token)
{
    Debug.Log(1);
    await UniTask.Delay(100, cancellationToken: token);
    Debug.Log(2);
}
```


## まとめ

`async/await`は便利な機能なので`Unity`でも使えるのはとてもうれしいです。
正しく理解して快適な`async/await`生活を送りましょう。

## 補足

コンパイラが生成する`async/await`のコードはもっと複雑です。
気になる方は`AsyncMethodBuilder`とか`AsyncStateMachine`について調べてみたり`ILSpy`などのツールで生成されたコードを調べてみてください。

以前書いた記事ですが、`async/await`を使えば以下のようなこともできます。
[async/awaitを使って継続の力を手に入れる](https://qiita.com/yaegaki/items/40d74b96163419ec29cf)