読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

任意の型を戻り値に持つ非同期メソッド

これまでの非同期メソッドは void / Task / Task<T> のいずれかを戻り値にしなければならないという制約がありました。

非同期メソッドが C# 5 で導入されてから早 4 年。もはやこれを「制約」と感じることはほとんどないくらい馴染んでしまっていますが、C# 7 でこの制約を取り払う機能が導入される予定です。ちなみにこの機能、公式には Arbitrary Async Returns とか Task-like (Task っぽい) という名称で呼ばれているようです。

f:id:xin9le:20160728155236p:plain

ValueTask<T>

ValueTuple<T> の記事でも多少説明しましたが、参照型はヒープ領域というメモリ空間を使用します。このメモリ空間の解放には若干重た目のガベージコレクション処理が走ります。一方値型はスタック領域というメモリ空間を使用し、こちらはより軽量な解放処理となります。これまで提供されてきた Task 型は参照型ですが、値型である ValueTask 型を提供することでいくつかのケースでのパフォーマンス改善が見込まれます。この ValueTask はすでに .NET Core に実装されています。

ValueTask は今回の主題である Task-like の機能に準拠しているため、以下のような感じで非同期メソッドが書けるようになります。

//--- Task 型以外の戻り値! 
async ValueTask<int> GetValueAsync()
{
    await Task.Delay(1000);
    return 123;
}

ValueTask は内部で Task を抱える実装をしているのですが、内包する Task を使うケースと使わないケースがあります。ValueTask にすることで効果が発揮されるのは、このうちの Task を使わないケースです。たとえば、以下のように await を通るか通らないかで変わります。

async ValueTask<int> DoSomethingAsync()
{
    var useInternalTask = true;
    if (useInternalTask)
    {
        //--- このコードパスは内包する Task を利用する
        await Task.Delay(1000);
        return 123;
    }

    //--- こっちのコードパスは内包する Task を利用しません
    return 456;
}

参照型の Task を利用しないコードパスを通ることが多いケースや、モデルの奥深くでだけ非同期処理が行われているような場合は ValueTask 型がパフォーマンスに大きく寄与すると思われます。

戻り値になれる型の条件

仮に MyTask 型を非同期メソッドの戻り値にするためには、まず以下のシグネチャを持つビルダークラスを実装している必要があります。

//--- コンパイラが非同期メソッドを実現するのに必要な機能が詰まった型
public class MyTaskBuilder
{
    public static MyTaskBuilder Create() => new MyTaskBuilder();

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

    public void SetStateMachine(IAsyncStateMachine stateMachine){}
    public void SetResult(){}
    public void SetException(Exception exception){}
    public MyTask Task => default(MyTask);

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

    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine
    {}
}

コンパイラはダックタイピング的に判定するため、何かの型を継承する必要はありません。MyTaskMyTaskBuilder に当たる部分の型名は任意なので、そのおかげで Task-like とする (= 任意の型を戻り値にする) ことができます。

どうしてこんな Builder が必要なのかは非同期メソッドを逆コンパイルしてみれば雰囲気が分かるかと思います。4 年前に内部実装について書いているのでそちらも参考にしてください。

戻り値にしたい型自体に属性を付与

このビルダー型がある前提で、以下のように戻り値にしたい型に AsyncMethodBuilder 属性を付けます。プロジェクト全体に対して適用したい場合に利用します。

//--- 非同期メソッドの戻り値となる型に対応するビルダーを属性で指定
[AsyncMethodBuilder(typeof(MyTaskBuilder))]
public class MyTask
{}

static async MyTask TestAsync()
{
    await Task.Delay(1000);
}

戻り値を変更したい非同期メソッド自体に属性を付与

この機能は将来実装される予定になっているものです。2016/10/10 現在では動作しません。こんな感じになるのでは?という推測で書いています。

非同期メソッドの戻り値をピンポイントでカスタムしたい場合には、メソッドに対して個別に AsyncMethodBuilder 属性を付与します。

//--- 非同期メソッドに戻り値に対応するビルダーを属性で指定
[AsyncMethodBuilder(typeof(MyTaskBuilder))]
static async MyTask TestAsync()
{
    await Task.Delay(1000);
}

型自体に属性を付与することは型定義がないできませんが、メソッドに付与する場合は型定義がなくてもできるのがポイントです。これにより既存の型に対して適用するための逃げ道ができます。

最小限の実装で書いてみる

先の条件を満たしつつ、ほぼほぼ最小限の実装で書いてみると以下のような感じになります。ちょっと意味わからん系かもしれませんが、このくらいしないと任意の型を非同期メソッドの戻り値にできません。若干というか結構ハードル高いです。

//--- 非同期メソッドの戻り値となる型に対応するビルダーを属性で指定
[AsyncMethodBuilder(typeof(MyTaskBuilder<>))]
public class MyTask<T>
{
    private Task<T> Task { get; }
    public T Result => this.Task.GetAwaiter().GetResult();
    public MyTask(Task<T> task){ this.Task = task; }
}

//--- コンパイラが非同期メソッドを実現するのに必要な機能が詰まった型 (MyTask<T> 専用)
public class MyTaskBuilder<T>
{
    private TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();

    public static MyTaskBuilder<T> Create() => new MyTaskBuilder<T>();

    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine
        => stateMachine.MoveNext();

    public void SetStateMachine(IAsyncStateMachine stateMachine){}
    public void SetResult(T result) => this.tcs.SetResult(result);
    public void SetException(Exception exception) => this.tcs.SetException(exception);
    public MyTask<T> Task => new MyTask<T>(this.tcs.Task);

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine
        => awaiter.OnCompleted(stateMachine.MoveNext);

    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion 
        where TStateMachine : IAsyncStateMachine
        => awaiter.OnCompleted(stateMachine.MoveNext);
}

既存の型を戻り値にする

例えば、WinRT / UWP 系アプリケーションや Reactive プログラミングとの相互運用性を高めるため以下のようなことをしたい場合があるかもしれません。この場合は独自ビルダーを作成してメソッドに対して属性を設定することで解決できます。もちろん標準ライブラリが IObservable<T>IAsyncActionAcyncMethodBuilder 属性を付けてくれれば、このような対応は不要になります。

IObservable<T> に対応

static void Main()
{
    //--- Reactive なシーケンス
    IObservable<T> sequence = SampleAsync();
}

//--- 対象の非同期メソッドに適用
[AsyncMethodBuilder(typeof(IObservableBuilder<>))]
static async IObservable<T> SampleAsync()
{
    await Task.Delay(100);
    return 123;
}

//--- async/await を IObservable<T> に対応させる
public class IObservableBuilder<T>
{
    public IObservable<T> Task => this.tcs.Task.ToObservable();
    //--- その他は前述の MyTaskBuilder<T> と同様なので省略
}

/*
//--- Subject<T> を使って書くとこんな感じ
public class IObservableBuilder<T>
{
    public static IObservableBuilder<T> Create() => new IObservableBuilder<T>();

    private Subject<T> subject = new Subject<T>();
    public void SetResult(T result)
    {
        this.subject.OnNext(result);
        this.subject.OnCompleted();
    }
    public void SetException(Exception exception) => this.subject.OnError(exception);
    public IObservable<T> Task => this.subject;

    //--- その他は前述の MyTaskBuilder<T> と同様なので省略
}
*/

IAsyncAction に対応

static void Main()
{
    //--- WinRT / UWP の非同期処理インターフェース
    IAsyncAction action = UwpAsync();
}

//--- 対象の非同期メソッドに適用
[AsyncMethodBuilder(typeof(IAsyncActionBuilder))]
static async IAsyncAction UwpAsync()
{
    await Task.Delay(100);
}

//--- async/await を IAsyncAction に対応させる
public class IAsyncActionBuilder
{
    public IAsyncAction Task => this.tcs.Task.AsAsyncAction();
    //--- その他は前述の MyTaskBuilder と同様なので省略
}

世界で 5 人だけ

以下のディスカッションにあるのですが、この機能は C# 7 に入りそうなものの「使うのは世界で 5 人ぐらいのものだろう」と言われています。非同期処理を独自の型で表現したいケースがどれほどあるのかということだと思いますが、まずないでしょう。なので、何か特殊な差し込みをしたいなどのことがない限り、この機能のことを知っている必要はないと思いますw