はじめに
C# 7.0 で導入された Generalized async return types(Task-like、arbitrary async returns、Async 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();
}
MyTask
を Task-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 typedynamic
-
t
has an accessible instance or extension method calledGetAwaiter
with no parameters and no type parameters, and a return typeA
for which all of the following hold:
本当に必要なのは AsyncMethodBuilder 属性
では「async の戻り値の型」に必要なモノって一体なんなのでしょうか?
その答えは Async Task Types in C# - dotnet/roslyn にありました。
A task type is a
class
orstruct
with an associated builder type identified withSystem.Runtime.CompilerServices.AsyncMethodBuilderAttribute
.
上記から AsyncMethodBuilder
属性と builder type なるものが必要だと判ります。
builder type が何かというと、
The builder type is a
class
orstruct
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
を実装してみます。
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
public struct MyTask<T> {
private Task<T> _task;
public MyTask(Task<T> task) => _task = task;
public T Result => _task.Result;
}
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
が無い環境であれば、以下を定義して下さい。
namespace System.Runtime.CompilerServices {
public sealed class AsyncMethodBuilderAttribute : Attribute {
public AsyncMethodBuilderAttribute(Type builderType) {
BuilderType = builderType;
}
public Type BuilderType { get; }
}
}
これで準備は整いました。
実際に使ってみましょう。
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 可能にしないとタスクっぽくないですよね。
という事で MyTask
に GetAwaiter
を足します。
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
public struct MyTask<T> {
...
public TaskAwaiter<T> GetAwaiter() => _task.GetAwaiter();
}
こうする事1で、async の戻り値を await する事ができるようになりました。
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
-
ValueTask
のようにパフォーマンスを出す為には必然的にAwaiter
も実装する事になるのですが、本稿の趣旨から逸れてしまうので割愛。 ↩