【C#】async/awaitの挙動をTask-Likeとawaitableパターンで変えてみる

C#
このエントリーをはてなブックマークに追加

卒論でそういうことをやったので書いておきます。(卒論で作ったプログラム)

まずはasync/awaitの基礎部分から(全部説明するとは言っていない)

public static async Task Sample()
{
    int simpleResult = await Simple();
    int distributedResult = await Distributed();
}

asyncメソッドはこんな感じ(結構おおざっぱ)ですけども、awaitキーワード(演算子?)の後ろ部分で何らかの処理(スレッドは問わない)を行い、そのawaitキーワードによってasyncメソッドの実行スレッドに返ってくる(正確には同期コンテキストがどのスレッドに戻るか決める)のですが、このawaitキーワードの後ろの部分はawaitableパターンならなんでもいいという話。
また、asyncメソッドの戻り値がTask型になっていますが、ここも(C# 7.0から)Task-Likeであればなんでもいいという話。

というわけで、Task-Likeとawaitableパターンを使えばasync/awaitの挙動を変えれるのですが、公式的にこれを利用しているのがValueTaskです。防御的プログラミングとかで早期リターンした場合などにわざわざTaskを起動させるまでもない場合に即時返却するのがValueTaskです。

Task-Like

まずTask-Likeのほうから。
Task-Likeするにはasyncメソッドの戻り値となるTask’型とそのTask`型がasyncメソッドの展開後どのような動きをするか組み立てるAsyncTaskMethodBuilder`型が必要です。(`は名前が被るので自作する方の型という意味です)

また、現時点でほとんどのプラットフォームではAsyncMethodBuilderAttribute属性も自分で定義する必要があります。

namespace System.Runtime.CompilerServices
{
    public sealed class AsyncMethodBuilderAttribute : Attribute
    {
        public AsyncMethodBuilderAttribute(Type builderType)
        {
            BuilderType = builderType;
        }

        public Type BuilderType { get; }
    }
}

こんな感じ。

Task`型とAsyncTaskMethodBuilder`型はダックタイピングな感じでするのでインターフェースの実装などはしなくてもいいですが、その分実装が間違っていた場合コンパイラーが謎のエラーを叩いてしまうので注意が必要です。

Task`型

[AsyncMethodBuilder(typeof(AsyncTaskMethodBuilder`<>))]
public class Task`<T>
{

    private readonly Task<T> _task;
    private readonly TaskAwaiter<T> _taskAwaiter;

    public Task`(Task<T> task)
    {
        _task = task ?? throw new ArgumentNullException(nameof(task));
        _taskAwaiter = new TaskAwaiter<T>(_task);
    }

    public Task<T> AsTask()
    {
        return _task;
    }

    public bool IsCompleted => _task.IsCompleted;

    public bool IsCompletedSuccessfully => _task.Status == TaskStatus.RanToCompletion;

    public bool IsFaulted => _task.IsFaulted;

    public bool IsCanceled => _task.IsCanceled;

    public TaskAwaiter<T> GetAwaiter() => _taskAwaiter;

    public T Result => _task.Result;
}

AsyncTaskMethodBuilder`型

public class AsyncTaskMethodBuilder`<T>
{
    private AsyncTaskMethodBuilder<T> methodBuilder;
    private Task`<T> task;

    public static AsyncTaskMethodBuilder`<T> Create()
    {
        return new AsyncTaskMethodBuilder`<T>() { methodBuilder = AsyncTaskMethodBuilder<T>.Create() };
    }

    public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
    {
    }

    public void SetStateMachine(IAsyncStateMachine stateMachine)
    {
        methodBuilder.SetStateMachine(stateMachine);
    }

    public void SetResult(T result)
    {
        methodBuilder.SetResult(result);
    }

    public void SetException(Exception exception)
    {
        methodBuilder.SetException(exception);
    }
    
    public Task`<T> Task {
        get {
            return task;
        }
    }

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine
    {
        methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine);
    }

    [SecuritySafeCritical]
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine
    {
        methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
    }
}

Task-Likeの最小構成ではないですが、こんな感じにAsyncTaskMethodBuilder型を利用してやれば車輪の再開発にはならないですし、複雑な状態機械処理を考えなくてすみます。

Awaitableパターン

awaitableパターンは比較的簡単でforeachのenumerableのアレと似てます。

public struct TaskAwaiter<TResult> : ICriticalNotifyCompletion
{
    private readonly Task<TResult> _task;

    internal TaskAwaiter(Task<TResult> task)
    {
        _task = task;
    }

    public bool IsCompleted {
        get { return _task.IsCompleted; }
    }

    [SecuritySafeCritical]
    public void OnCompleted(Action continuation)
    {
        continuation();
    }

    [SecurityCritical]
    public void UnsafeOnCompleted(Action continuation)
    {
        continuation();
    }

    public TResult GetResult()
    {
        return _task.Result;
    }
}

こんな感じにして、Task`型にGetAwaiterメソッドを生やしてやっておけばいいです。
また、Task`型によってはTaskAwaiterを自作する必要がないこともあるかと思います。Task型を利用しているとかで。

おわりに

Task-Likeとawaitableパターンでasync/awaitの挙動変えることはまずしないと思います。(自分がしたのはただやりたかったからです)
また、テストするのも大変なのでコアライブラリでしか使われないと思います。(ReactiveExtensionsとか)
もし、実装する場合は僕の卒論プログラムも結構参考になるんじゃないかなと思いますのでちょっとは覗いてやってください()

来週は何の記事書こうかな。。