[C#]async/awaitを使って継続の力を手に入れる

`async/await` は非同期処理を同期処理のように書ける機能です。
しかし、`async/await`にはそれ以上のポテンシャルがあります。
それが **継続の取得** です。
`AsyncMethodBuilder`と`async/await`を併用することで **継続の取得**を行うことができます。

今回は**継続の取得**を~~悪用~~使用する拙作ライブラリ`Async.Chaos`の紹介と解説をします。

[yaegaki/Async.Chaos](https://github.com/yaegaki/Async.Chaos)

## 注意

**実用的な内容ではないネタ記事です。**
こんなこともできるのか、くらいの感じで読んでください。
`async/await`に自信がある方は解説を読む前にどのようにして実現しているのかを考えてみるのも面白いかもしれません。
コンパイラの暗黙の挙動に頼っている部分もあるのでバージョンが変わると動かない可能性があります。

## 使用例

`Async.Chaos`の使用例です。

### 再帰処理

一番シンプルな使用例です。
`ChaosTask.Continuation`で継続を取得して再帰的に呼び出します。

```csharp
// 最大公約数を計算する
static async ChaosTask<int> gcd(int _a, int _b)
{
	// 引数と継続を取得する
    var (a, b, continuation) = await ChaosTask.Continuation<int, int, int>(_a, _b);

    if (a % b == 0)
    {
        return b;
    }
    
    // 取得した継続で再帰する
    return await continuation(b, a % b);
}
```

```csharp
Console.WriteLine(await gcd(824, 128)); // => 8
```

### チェックポイント

`ChaosTask.Checkpoint`でコードにチェックポイントを作成し、任意の場合からチェックポイントに戻ります。

```csharp
// 指定された長さのリストを取得する
// 内容は0~lenまでの数値がランダムな順番にかぶりなしで入っている
static async ChaosTask<List<int>> GetNumbersInRandomOrder(int len)
{
    var r = new Random();
    var result = new List<int>(len);
    // チェックポイントの作成
    var checkpoint = await ChaosTask.Checkpoint<List<int>>();

    // リストの構築が終わっていない場合
    if (result.Count < len)
    {
        // ランダムな値取得
        var value = r.Next(0, len);
        // 値が重複しないように存在しない場合だけ追加する
        if (!result.Contains(value))
        {
            result.Add(value);
        }

        // チェックポイントに戻る
        await checkpoint();
    }

    return result;
}

```

```csharp
var list = await GetNumbersInRandomOrder(100);
Console.WriteLine(string.Join(",", list));  // => 27,54,70,4,7,28,21,93,32,69,83,45,22,...
```

### Concurrent(Simple)

`ChaosTask.Concurrent`で継続を並行で実行します。
同時に実行されるわけではなく常に順番に実行されます。

```csharp
static async ChaosTask<ChaosUnit> SimpleConcurrentTest()
{
    // 並行で継続を実行する
    // 継続は親、子の二回実行される
    // 親か子かはConcurrentの戻り値で判定することができる
    if (await ChaosTask.Concurrent<ChaosUnit>())
    {
        Console.WriteLine("Parent.");
    }
    else
    {
        Console.WriteLine("Child.");
    }
    return default(ChaosUnit);
}
```

```csharp
await SimpleConcurrentTest();
/* =>
Parent.
Child.
*/
```

### Concurrent(EchoServer)

`ChaosTask.Concurrent`は処理を並行で実行するため、一つのタスクが処理をブロックするとほかのタスクも影響を受けます。
`ChaosTask.Yield`や`ChaosTask.WaitTask`、`ChaosTask.WaitNext`を使用することで別タスクの処理を行います。

```csharp
static async ChaosTask<string> StartEchoServer()
{
    Console.WriteLine("Start echo server.");
    Console.WriteLine("if you want to exit, Type 'end'.");
    // 値型はタスクごとにコピーが作られるので参照型でラップする
    var finished = ChaosBox.Create(false);
    var message = string.Empty;
    // 並行タスク開始
    var isParent = await ChaosTask.Concurrent<string>();

    // 親はメッセージを受け付ける
    if (isParent)
    {
        while (!finished.Value)
        {
            // 他のタスクが待機状態になるまで待つ
            await ChaosTask.WaitNext<string>();
            if (!string.IsNullOrEmpty(message))
            {
                if (message == "end")
                {
                    Console.WriteLine("byebye");
                    finished.Value = true;
                }
                else
                {
                    Console.WriteLine($"your message:{message}");
                }
            }
        }

        // 他のタスクの終了を待つ
        await ChaosTask.Yield<string>();
        return message;
    }
    // 子はメッセージを送る
    else
    {
        while (!finished.Value)
        {
            async ChaosTask<ChaosUnit> sendMessage()
            {
                await Task.Run(() =>
                {
                    message = Console.ReadLine();
                });
                return default(ChaosUnit);
            }

            // タスクの実行が終わってから並行処理を再開する
            await ChaosTask.WaitTask<string>(sendMessage());
        }
        return string.Empty;
    }
}
```

```csharp
await StartEchoServer();
/* =>
Start echo server.
if you want to exit, Type 'end'.
hoge
your message:hoge
fuga
your message:fuga
piyo
your message:piyo
end
byebye
*/
```

## 解説

基本的なコンセプトは継続の取得と実行です。
`Awaiter`を使うと`OnCompleted`で継続を取得できますがここで取得できる継続はそのままでは再利用できません。

```csharp
class Awaiter
{
    /*
       省略...
    */
    public void OnCompleted(Action continuation)
    {
        // continuation がawait以降の継続を表すアクション
    }
}
```

再利用できない理由はここで取得できる継続が`ステート`を持っているからです。
一回呼ぶと継続の内部`ステート`が変更されてしまうのでもう一度呼んでも違う結果になります。

なので、`ステート`の保存と復元ができるなら何度でも使用できる継続を取得することができます。
ではこの`OnCompleted`で渡される継続はどこから来たものでしょうか。
答えはコンパイラが自動生成したステートマシンの`MoveNext`です。

以下の例を見てください。

```csharp
static async Task Hoge()
{
    var i = 0;
    await Task.Delay(100);
    Console.WriteLine(i++);
    await Task.Delay(100);
    Console.WriteLine(i++);
}
```

この関数からは以下のステートマシンが生成されます。

```csharp
[CompilerGenerated]
private sealed class <Hoge>d__1 : IAsyncStateMachine
{
	public int <>1__state;

	public AsyncTaskMethodBuilder <>t__builder;

	private int <i>5__1;

	private TaskAwaiter <>u__1;

	void IAsyncStateMachine.MoveNext()
	{
		int num = this.<>1__state;
		try
		{
			TaskAwaiter awaiter;
			TaskAwaiter awaiter2;
			if (num != 0)
			{
				if (num == 1)
				{
					awaiter = this.<>u__1;
					this.<>u__1 = default(TaskAwaiter);
					this.<>1__state = -1;
					goto IL_F2;
				}
				this.<i>5__1 = 0;
				awaiter2 = Task.Delay(100).GetAwaiter();
				if (!awaiter2.IsCompleted)
				{
					this.<>1__state = 0;
					this.<>u__1 = awaiter2;
					Program.<Hoge>d__1 <Hoge>d__ = this;
					this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Hoge>d__1>(ref awaiter2, ref <Hoge>d__);
					return;
				}
			}
			else
			{
				awaiter2 = this.<>u__1;
				this.<>u__1 = default(TaskAwaiter);
				this.<>1__state = -1;
			}
			awaiter2.GetResult();
			int num2 = this.<i>5__1;
			this.<i>5__1 = num2 + 1;
			Console.WriteLine(num2);
			awaiter = Task.Delay(100).GetAwaiter();
			if (!awaiter.IsCompleted)
			{
				this.<>1__state = 1;
				this.<>u__1 = awaiter;
				Program.<Hoge>d__1 <Hoge>d__ = this;
				this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Hoge>d__1>(ref awaiter, ref <Hoge>d__);
				return;
			}
			IL_F2:
			awaiter.GetResult();
			num2 = this.<i>5__1;
			this.<i>5__1 = num2 + 1;
			Console.WriteLine(num2);
		}
		catch (Exception exception)
		{
			this.<>1__state = -2;
			this.<>t__builder.SetException(exception);
			return;
		}
		this.<>1__state = -2;
		this.<>t__builder.SetResult();
	}

	[DebuggerHidden]
	void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
	{
	}
}
```

このステートマシンの`MoveNext`が継続の正体です。
みればわかりますが、`MoveNext`が呼ばれると内部の変数が変更されます。

ステートマシンのコピーは`Awaiter`では実現できません。
コピーできるタイミングとして`AsyncMethodBuilder`が考えられます。
`AsyncMethodBuilder`は任意の型を`async`関数の戻り値にするためのものです。
`AsyncMethodBuilder`には`Awaiter`と`StateMachime`が渡される`AwaitUnsafeOnCompleted`という関数があります。
ここがコピーを行うことのできるタイミングです。

```csharp
class AsyncHogeMethodBuilder
{
    /*
        省略
    */
    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine
    {
        // ここに来た時点(awaitされた時点)でのstateMachineのコピー
        // コピーにはリフレクションをつかってMemberwiseClonseを呼び出す
        var copy = CopyStateMachine(stateMacine);
        // 何度も呼び出し可能な継続を作る
        var continuation = () =>
        {
            // 何度呼び出されてもいいようにもう一度コピーしてそっちを使用する
            var _copy = CopyStateMachine(copy);
            _copy.MoveNext();
        };

        // 継続を使って何かする

        // 省略...
    }
}
```

これで何度も呼び出すことが可能な継続を手に入れることはできました。
次に問題になるのが`await`したときの戻り値です。
`await`したときの戻り値は`Awaiter`の`GetResult`が使用されます。
よって以下のように`MoveNext`の前に`Awaiter`に準備させます。

```csharp
var prepare = awaiter.GetPrepare(); // awaiterには何かしらのインターフェースを継承させておく
var continuation = () =>
{
    // 戻り値を準備させる
    // 実際は戻り値をどのようにして準備させるかによってもっと複雑になる
    prepare();
    // 何度呼び出されてもいいようにもう一度コピーしてそっちを使用する
    var _copy = CopyStateMachine(copy);
    _copy.MoveNext();
};
```

以上で基本的な解説は終わりです。
後は継続をどうやって作るか、どうやって継続を管理するかの問題になります。
あまり面白い話でもないので省略します。

コードとしてはこの辺にあります。(上記のものとは結構違いますが基本的なコンセプトは同じです。)
[AsyncChaosMethodBuilder.cs](https://github.com/yaegaki/Async.Chaos/blob/master/Async.Chaos/AsyncChaosMethodBuilder.cs)

## 最後に

`Async.Chaos`はパフォーマンスの面でも機能面でもあまり意味のないライブラリです。
(しかもスケジューラがおそらくバグっているのでスレッドが複数になってくるとうまく動かないかもしれない)
しかし、今の`C#`ではやろうと思えばこんなこともできるというのは面白いと思いました。