LoginSignup
3
3

More than 5 years have passed since last update.

Generalized async return types (Task-like) の最小実装

Posted at

はじめに

C# 7.0 で導入された Generalized async return typesTask-likearbitrary async returnsAsync Task Type とも)。

ValueTask を使用する事でその恩恵を受ける事ができますが、その仕組みは汎化されていますので、独自の Task-like を定義する事も可能です。
(かといって、実務で不必要に Task-like を作ると複雑度が上がるだけなのでお勧めしませんが)

この記事では、理解を深める事を主目的として、独自の Task-like の最小実装を示します。

GetAwaiter があれば良い?

公式リファレンスを参照すると以下のように記されています。簡単そうですね。

戻り値の型

非同期メソッドの戻り値の型を次に示します。

  • Task
  • Task<TResult>
  • void: イベント ハンドラーに対してのみ使用します。
  • C# 7 以降、アクセス可能な GetAwaiter を持つ任意の型です。 System.Threading.Tasks.ValueTask<TResult> 型はこの実装例で、 NuGet パッケージ System.Threading.Tasks.Extensions を追加することで使用できます。

(中略)

C# 7 以降、GetAwaiter メソッドを持つ別の型 (通常は値の型) を返して、コードのパフォーマンスが重要なセクションでメモリ割り当てを最小限に抑えます。

「これだけ?」と思わずにはいられない情報量ですが、百聞は一見に如かず、実際にコードを記述してみましょう。
ここで GetValueAsync は「非同期に 1 秒待って 123 を返す」だけのメソッドです。

public static async MyTask<int> GetValueAsync() {
    await Task.Delay(1000);
    return 123;
}

public struct MyTask<T> {
    public MyTask(T result) => Result = result;

    public T Result { get; }

    public TaskAwaiter<T> GetAwaiter() => Task.FromResult(Result).GetAwaiter();
}

MyTaskTask-like とする為、シンプルな TaskAwaiter を返す GetAwaiter を実装してビルドします。
すると、

CS1983 The return type of an async method must be void, Task or Task<T>

となりました。

アレ?

GetAwaiter とは

アクセス可能な GetAwaiter

を持たせただけでは Task-like として認められませんでした。

一方で、以下のコードは実行できます。
(async の戻り値の型を Task にしてしまっているので、MyTask を挟む意味は全くないのですが)

public static async /*My*/Task<int> GetValueAsync() {
    await Task.Delay(1000);
    //return 123;
    return await new MyTask<int>(123);
}

public struct MyTask<T> {
    public MyTask(T result) => Result = result;

    public T Result { get; }

    public TaskAwaiter<T> GetAwaiter() => Task.FromResult(Result).GetAwaiter();
}

つまり、GetAwaiter は任意の型を await 可能にする為に必要 であって、「async の戻り値の型」には必須ではないのですね。

「C# 言語仕様」を紐解くと、「7.7.7.1 待機可能な式」に GetAwaiter の事が記載されています(Web 版はこちら)。

await 式のタスクは、待機可能である必要があります。式 t は、次のいずれかに該当する場合、待機可能です。

  • t is of compile time type dynamic
  • t has an accessible instance or extension method called GetAwaiter with no parameters and no type parameters, and a return type A for which all of the following hold:

本当に必要なのは AsyncMethodBuilder 属性

では「async の戻り値の型」に必要なモノって一体なんなのでしょうか?

その答えは Async Task Types in C# - dotnet/roslyn にありました。

A task type is a class or struct with an associated builder type identified with System.Runtime.CompilerServices.AsyncMethodBuilderAttribute.

上記から AsyncMethodBuilder 属性と builder type なるものが必要だと判ります。

builder type が何かというと、

The builder type is a class or struct that corresponds to the specific task type. The builder type has the following public methods. For non-generic builder types, SetResult() has no parameters.

とあり、builder type は下記の形式のメンバーを持っていると書かれています。

class MyTaskMethodBuilder<T>
{
    public static MyTaskMethodBuilder<T> Create();

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

    public void SetStateMachine(IAsyncStateMachine stateMachine);
    public void SetException(Exception exception);
    public void SetResult(T result);

    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;

    public MyTask<T> Task { get; }
}

最小実装

上記を踏まえ、改めて MyTask を実装してみます。

MyTask.cs
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
public struct MyTask<T> {
    private Task<T> _task;

    public MyTask(Task<T> task) => _task = task;

    public T Result => _task.Result;
}
MyTaskMethodBuilder.cs
public struct MyTaskMethodBuilder<T> {
    private AsyncTaskMethodBuilder<T> _methodBuilder;

    public static MyTaskMethodBuilder<T> Create() =>
        new MyTaskMethodBuilder<T> { _methodBuilder = AsyncTaskMethodBuilder<T>.Create() };

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

    public void SetStateMachine(IAsyncStateMachine stateMachine) {
        _methodBuilder.SetStateMachine(stateMachine);
    }
    public void SetException(Exception exception) {
        _methodBuilder.SetException(exception);
    }
    public void SetResult(T result) {
        _methodBuilder.SetResult(result);
    }

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

    public MyTask<T> Task => new MyTask<T>(_methodBuilder.Task);
}

ValueTask の実装をベースに記述していますので、興味のある方はソースも見てみて下さい
(この実装だと結局 Task が生成されてしまうので、パフォーマンスには寄与しませんし。ただのラッパー……)

もし System.Runtime.CompilerServices.AsyncMethodBuilderAttribute が無い環境であれば、以下を定義して下さい。

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

        public Type BuilderType { get; }
    }
}

これで準備は整いました。
実際に使ってみましょう。

Program.cs
static void Main() {
    var result = GetValueAsync().Result;
    Console.WriteLine(result);
}

public static async MyTask<int> GetValueAsync() {
    await Task.Delay(1000);
    return 123;
}

今度はビルドも成功し、標準出力に 123 と出力されました!

サンプルコード

上記コードを LinqPad ファイルとしてまとめましたので、手っ取り早く確認したい方はこちらをご参照ください。
C# 7.0 Generalized async return types - Minimum Implementation Code for LinqPad 5

GetAwaiter? 知らない子ですね

別に GetAwaiter が無くても Generalized async return types は実現できましたね!

--- 完 ---

でも良いのですが、Task-like という場合はきちんと await 可能にしないとタスクっぽくないですよね。
という事で MyTaskGetAwaiter を足します。

MyTask.cs
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
public struct MyTask<T> {
    ...
    public TaskAwaiter<T> GetAwaiter() => _task.GetAwaiter();
}

こうする事1で、async の戻り値を await する事ができるようになりました。

Program.cs
static async Task Main() {
    var result = await GetValueAsync();
    Console.WriteLine(result);
}

public static async MyTask<int> GetValueAsync() {
    await Task.Delay(1000);
    return 123;
}

チャンチャン

See also

async (C# リファレンス) | Microsoft Docs
非同期の戻り値の型 (C#) | Microsoft Docs
Async Task Types in C# - dotnet/roslyn
corefx/ValueTask.cs at master · dotnet/corefx
任意の型を戻り値に持つ非同期メソッド - xin9le.net


  1. ValueTask のようにパフォーマンスを出す為には必然的に Awaiter も実装する事になるのですが、本稿の趣旨から逸れてしまうので割愛。 

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3