非同期ストリーム
手記
この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。
機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。
機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。
概要
C# では反復子メソッドと非同期メソッドがサポートされていますが、反復子と非同期メソッドの両方であるメソッドはサポートされません。 これを修正するには、await
を新しい形式の async
反復子として使用し、IEnumerable<T>
や IEnumerator<T>
の代わりに IAsyncEnumerable<T>
または IAsyncEnumerator<T>
を返す形にし、新しい await foreach
で IAsyncEnumerable<T>
を使用できるようにする必要があります。 IAsyncDisposable
インターフェイスは、非同期クリーンアップを有効にするためにも使用されます。
関連するディスカッション
詳細な設計
インターフェイス
IAsyncDisposable
IAsyncDisposable
(https://github.com/dotnet/roslyn/issues/114など) と、それが良いアイデアかどうかについて多くの議論が行われています。 ただし、非同期反復子のサポートを追加するために必要な概念です。 finally
ブロックには await
が含まれている可能性があるため、finally
ブロックは反復子の破棄の一部として実行する必要があるため、非同期破棄が必要です。 リソースのクリーンアップに時間がかかる可能性があるときに、たとえば、ファイルを閉じる(フラッシュが必要)、コールバックの登録解除や、登録解除が完了したことを知る方法を提供する場合など、一般的に役立ちます。
次のインターフェイスがコア .NET ライブラリ (System.Private.CoreLib/System.Runtime など) に追加されます。
namespace System
{
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
}
Dispose
と同様に、DisposeAsync
を複数回呼び出してもかまいません。1 回目以降の呼び出しは操作なしとして扱う必要があります。同期的に完了した成功したタスクを返します (DisposeAsync
スレッド セーフである必要はなく、同時呼び出しをサポートする必要もありません)。 さらに、型は IDisposable
と IAsyncDisposable
の両方を実装できます。その場合、Dispose
を呼び出してから DisposeAsync
またはその逆を呼び出すのと同様に受け入れられますが、最初の呼び出しだけが意味があり、それ以降の呼び出しのみが nop である必要があります。 そのため、型が両方を実装する場合、コンシューマーは、コンテキストに基づいてより関連性の高いメソッドを 1 回だけ呼び出し、同期コンテキストで Dispose
し、非同期メソッドで DisposeAsync
することをお勧めします。
(IAsyncDisposable
が using
とやり取りする方法は別の説明です。また、foreach
との対話方法については、この提案の後半で取り扱います)。
考えられる代替手段:
を受け入れる :理論的には非同期的なものは取り消すことができるのは理にかなっているが、廃棄はクリーンアップ、終了、リソースの解放などであり、一般的にはキャンセルすべきものではない。クリーンアップは、取り消される作業にとって依然として重要です。 実際の作業を取り消す原因となったのと同じ CancellationToken
は、通常、DisposeAsync
に渡されるトークンと同じになり、作業の取り消しによってDisposeAsync
が no-opになるため、DisposeAsync
価値がありません。 破棄を待機するブロックを回避したい場合は、結果として得られるValueTask
を待つのを避けるか、一定期間だけ待機することができます。を返す : 非ジェネリック が存在し、 から構築できるようになったので、 から を返すと、既存のオブジェクトを の最終的な非同期完了を表す promise として再利用でき、 が非同期で完了した場合に 割り当てが保存されます。 bool continueOnCapturedContext
(ConfigureAwait
) を使用したDisposeAsync
の構成: このような概念がusing
、foreach
、およびこれを使用する他の言語コンストラクトに公開される方法に関連する問題がある可能性がありますが、インターフェイスの観点からは、実際にはawait
を実行していないため、構成する必要はありません。ValueTask
の消費者は、彼らが望むようにそれを消費することができます。を継承 : いずれか一方のみを使用する必要があるため、型に両方を強制的に実装することは意味がありません。 ではなく : モノ/型は "非同期の何か" という名前に従ってきましたが、操作は "done async" であるため、型はプレフィックスとして "Async" を持ち、メソッドはサフィックスとして "Async" を持ちます。
IAsyncEnumerable/IAsyncEnumerator
コア .NET ライブラリには、次の 2 つのインターフェイスが追加されます。
namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
ValueTask<bool> MoveNextAsync();
T Current { get; }
}
}
(追加の言語機能を使用しない) 一般的な消費量は次のようになります。
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
Use(enumerator.Current);
}
}
finally { await enumerator.DisposeAsync(); }
検討されたが破棄されたオプション。
Task<bool> MoveNextAsync(); T current { get; }
:Task<bool>
を使用すると、キャッシュされたタスク オブジェクトを使用して同期的で成功したMoveNextAsync
呼び出しを表すのがサポートされますが、非同期の完了には割り当てが必要です。ValueTask<bool>
を返すことで、列挙子オブジェクト自体がIValueTaskSource<bool>
を実装し、MoveNextAsync
から返されるValueTask<bool>
のバッキングとして使用できるようになり、オーバーヘッドが大幅に削減されます。ValueTask<(bool, T)> MoveNextAsync();
: 消費するのが難しいだけでなく、T
が共変ではなくなったことを意味します。ValueTask<T?> TryMoveNextAsync();
: 共変ではありません。Task<T?> TryMoveNextAsync();
: 共変ではなく、すべての呼び出しに対する割り当てなどITask<T?> TryMoveNextAsync();
: 共変ではなく、すべての呼び出しに対する割り当てなどITask<(bool,T)> TryMoveNextAsync();
: 共変ではなく、すべての呼び出しに対する割り当てなど-
Task<bool> TryMoveNextAsync(out T result);
:out
の結果は、操作が非同期的にタスクを完了したときではなく、同期的に戻されたときに設定する必要があります。そのため、タスクが将来のいつ完了するか分からない場合、結果を伝達する方法はありません。 を実装していない : これらを分離することもできます。 ただし、これを行うと、コードが列挙子が破棄を提供しない可能性に対処できる必要があるため、提案の他の特定の領域が複雑になり、パターンベースのヘルパーを記述することが困難になります。 さらに、列挙子には破棄の必要性 (たとえば、finally ブロックを持つ C# 非同期反復子、ネットワーク接続からのデータを列挙するものなど) が必要になるのが一般的です。そうでない場合は、追加のオーバーヘッドを最小限に抑えて public ValueTask DisposeAsync() => default(ValueTask);
としてメソッドを簡単に実装できます。- _
IAsyncEnumerator<T> GetAsyncEnumerator()
: キャンセル トークン パラメーターがありません。
次のサブセクションでは、選択されなかった代替方法について説明します。
実行可能な代替手段:
namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator();
}
public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
ValueTask<bool> WaitForNextAsync();
T TryGetNext(out bool success);
}
}
TryGetNext
は、同期的に使用できる限り、1 つのインターフェイス呼び出しで項目を使用するために内部ループで使用されます。 次の項目を同期的に取得できない場合、false を返し、false を返すたびに、呼び出し元は、次の項目が使用可能になるのを待つか、別の項目が存在しないことを判断するために、WaitForNextAsync
を呼び出す必要があります。 (追加の言語機能を使用しない) 一般的な消費量は次のようになります。
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.WaitForNextAsync())
{
while (true)
{
int item = enumerator.TryGetNext(out bool success);
if (!success) break;
Use(item);
}
}
}
finally { await enumerator.DisposeAsync(); }
この利点は2つあり、小さいものと大きいものがあります。
- マイナー: 列挙子が複数のコンシューマーをサポートできるようにします。 列挙子が複数の同時実行コンシューマーをサポートすることが重要なシナリオがある場合があります。
MoveNextAsync
とCurrent
が分離されている場合、実装で使用をアトミックにすることはできません。 これに対し、この方法では、列挙子を前方にプッシュして次の項目を取得することをサポートする 1 つのメソッドTryGetNext
が提供されるため、必要に応じて列挙子がアトミック性を有効にすることができます。 ただし、このようなシナリオは、共有列挙可能な列挙子から各コンシューマーに独自の列挙子を与えることによっても有効にできる可能性があります。 さらに、すべての列挙子が同時使用をサポートするように強制することは望ましくありません。これは、それを必要としない大多数のケースに簡単ではないオーバーヘッドを追加するためです。つまり、インターフェイスのコンシューマーは一般的にこれに依存できません。 - 主要: パフォーマンス.
MoveNextAsync
/Current
アプローチでは、操作ごとに 2 つのインターフェイス呼び出しが必要ですが、WaitForNextAsync
/TryGetNext
の最適なケースは、ほとんどのイテレーションが同期的に完了し、TryGetNext
でタイトな内部ループを有効にすることです。そのため、操作ごとにインターフェイス呼び出しは 1 つだけです。 これは、インターフェイス呼び出しが計算を支配する状況で測定可能な影響を与える可能性があります。
ただし、これらを手動で扱う場合の複雑さが著しく増し、それに伴いバグを発生させる可能性が高くなるなど、注意が必要な欠点があります。 また、パフォーマンス上の利点はマイクロベンチマークに現れますが、実際の使用の大部分で影響を受けるとは考えていません。 それが判明した場合は、2 つ目のインターフェイスセットを明るく導入できます。
検討された末に破棄されたオプション。
ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result);
:out
パラメーターを共変にすることはできません。 参照型の結果に対してランタイム書き込みバリアが発生する可能性が高いという小さな影響もあります(これは try パターン全体に共通する問題です)。
キャンセル
キャンセルをサポートするには、いくつかの方法があります。
IAsyncEnumerable<T>
/IAsyncEnumerator<T>
はキャンセルに依存しません。CancellationToken
はどこにも表示されません。 キャンセルは、反復子を呼び出すとき、CancellationToken
を反復子メソッドに引数として渡し、他のパラメーターと同様に反復子の本体で使用するなど、適切な方法でCancellationToken
を列挙可能な列挙子に論理的にベイクすることで実現されます。IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken)
:CancellationToken
をGetAsyncEnumerator
に渡すと、後続のMoveNextAsync
操作は可能な限りそれを考慮します。-
IAsyncEnumerator<T>.MoveNextAsync(CancellationToken)
: 個々のCancellationToken
呼び出しにMoveNextAsync
を渡します。 - 1 && 2:
CancellationToken
を列挙可能型または列挙子に埋め込み、CancellationToken
をGetAsyncEnumerator
に渡します。 - 1 && 3:
CancellationToken
を列挙可能/列挙子に埋め込み、CancellationToken
をMoveNextAsync
に渡します。
純粋に理論的な観点から見ると、(5) は最も堅牢です。(a) MoveNextAsync
CancellationToken
を受け入れることで、キャンセルされた内容を最もきめ細かく制御でき、(b) CancellationToken
は、任意の型に埋め込まれた反復子に引数として渡すことができる他の型にすぎません。
ただし、このアプローチには複数の問題があります。
GetAsyncEnumerator
に渡されたCancellationToken
は、反復子の本体にどのように変換されますか?iterator
に渡されたCancellationToken
にアクセスするために新しいGetEnumerator
キーワードを導入し、それを使ってアクセスすることも可能ですが、a) それには多くの追加の処理が必要であり、b) これを非常に優先度の高い要素としています。c) 99% ケースで、イテレーターを呼び出し、かつそれにGetAsyncEnumerator
を呼び出すための同一のコードに見えるため、その場合はCancellationToken
をメソッドの引数として渡すことができます。MoveNextAsync
に渡されたCancellationToken
はどのようにしてメソッドの本体に入りますか? これはさらに悪いことに、iterator
ローカル オブジェクトから公開されているかのように、その値は await 間で変更される可能性があります。つまり、トークンに登録されたコードは、待機する前に登録を解除してから、後で再登録する必要があります。また、反復子でコンパイラによって実装されているか、開発者によって手動で実装されているかに関係なく、すべてのMoveNextAsync
呼び出しでこのような登録と登録解除を行う必要がある場合は、非常にコストがかかる可能性があります。- 開発者が
foreach
ループを取り消す方法 列挙可能/列挙子にCancellationToken
を与えることによって行われる場合は、その後、a)foreach
をサポートする必要があります。これにより、これを非常に優先度の高い要素となり、列挙子 (LINQ メソッドなど) に基づいて構築されたエコシステムについて考え始める必要があります。b) 提供されたトークンを格納するCancellationToken
のWithCancellation
拡張メソッドを使用して、列挙型にIAsyncEnumerable<T>
を埋め込む必要があります。返された構造体のGetAsyncEnumerator
が呼び出されたときに、ラップされた列挙可能なGetAsyncEnumerator
に渡します (そのトークンは無視されます)。 または、foreach の本体に含まれるCancellationToken
だけを使用できます。 - クエリの理解がサポートされる場合またはされる時に、
CancellationToken
をGetEnumerator
やMoveNextAsync
に指定して、各句に渡すにはどうすればよいでしょうか。 最も簡単な方法は、句がそれをキャプチャすることで、その結果、GetAsyncEnumerator
/MoveNextAsync
に渡されるトークンは無視されることになります。
このドキュメントの以前のバージョンでは (1) をお勧めしますが、(4) に切り替えました。
(1) の 2 つの主な問題:
- 取り消し可能な列挙体のプロデューサーは、いくつかの定型句を実装する必要があり、非同期反復子に対するコンパイラのサポートを利用して
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken)
メソッドを実装することしかできません。 - 多くのプロデューサーは、代わりに非同期列挙可能なシグネチャに
CancellationToken
パラメーターを追加したくなる可能性があります。これにより、コンシューマーがIAsyncEnumerable
型を指定したときに必要なキャンセル トークンを渡すことができなくなります。
主に次の 2 つの消費シナリオがあります。
- コンシューマーが非同期イテレーター メソッドを呼び出す
await foreach (var i in GetData(token)) ...
- コンシューマーが特定の
IAsyncEnumerable
インスタンスを扱う場所がawait foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ...
です。
非同期ストリームのプロデューサーとコンシューマーの両方にとって便利な方法で両方のシナリオをサポートする妥当な妥協点は、非同期反復子メソッドで特別に注釈付けされたパラメーターを使用することです。 この目的には、[EnumeratorCancellation]
属性が使用されます。 この属性をパラメーターに配置すると、トークンが GetAsyncEnumerator
メソッドに渡された場合は、パラメーターに最初に渡された値ではなく、そのトークンを使用する必要があることをコンパイラに指示します。
IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default)
を検討してください。
このメソッドの実装者は、メソッド本体で単にパラメーターを使用できます。
コンシューマーは、上記のいずれかの消費パターンを使用できます。
GetData(token)
を使用する場合、トークンは非同期列挙可能に保存され、イテレーションで使用されます。givenIAsyncEnumerable.WithCancellation(token)
を使用する場合、GetAsyncEnumerator
に渡されたトークンは、非同期列挙可能に保存されたトークンよりも優先されます。
foreach
foreach
は、IEnumerable<T>
の既存のサポートに加えて、IAsyncEnumerable<T>
をサポートするように拡張されます。 関連するメンバーがパブリックに公開されている場合、パターンとして IAsyncEnumerable<T>
に相当するものをサポートします。公開されていない場合は、インターフェイスを直接使用します。これにより、割り当てを回避し、MoveNextAsync
および DisposeAsync
を戻り値型として代替の待機可能型を使用するできる構造体ベースの拡張を有効にします。
構文
構文の使用:
foreach (var i in enumerable)
C# は enumerable
を同期列挙可能として引き続き扱います。したがって、非同期列挙体に関連する API が公開されている場合でも (パターンを公開したり、インターフェイスを実装したりする)、同期 API のみが考慮されます。
foreach
が非同期 API のみを考慮させるために、await
は次のように挿入します。
await foreach (var i in enumerable)
非同期 API または同期 API の使用をサポートする構文は提供されません。開発者は、使用する構文に基づいて選択する必要があります。
意味論
await foreach
ステートメントのコンパイル時の処理では、まず、式の コレクション型、列挙子型 、および 反復型 が決定されます (https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/statements.md#1395-the-foreach-statementとよく似ています)。 この決定は次のように進みます。
- 式 の型
X
がdynamic
または配列型の場合、エラーが生成され、それ以上の手順は実行されません。 - それ以外の場合は、
X
型に適切なGetAsyncEnumerator
メソッドがあるかどうかを判断します。- 識別子
GetAsyncEnumerator
を持ち、型引数を持たない型X
に対してメンバー参照を実行します。 メンバー参照で一致が生成されない場合、またはあいまいさが生成される場合、またはメソッド グループではない一致が生成される場合は、次に説明するように列挙可能なインターフェイスを確認します。 - 結果のメソッド グループと空の引数リストを使用して、オーバーロードの解決を実行します。 オーバーロードの解決によって該当するメソッドが存在しない場合、あいまいになる場合、または単一の最適なメソッドになるが、そのメソッドが静的であるかパブリックでない場合は、次に説明するように列挙可能なインターフェイスを確認します。
GetAsyncEnumerator
メソッドの戻り値の型E
がクラス、構造体、またはインターフェイス型でない場合は、エラーが生成され、それ以上の手順は実行されません。- メンバー参照は、識別子
Current
を持ち、型引数を持たないE
に対して実行されます。 メンバー参照で一致が生成されない場合、結果がエラーであるか、読み取りを許可するパブリック インスタンス プロパティ以外の結果である場合は、エラーが生成され、それ以上の手順は実行されません。 - メンバー参照は、識別子
MoveNextAsync
を持ち、型引数を持たないE
に対して実行されます。 メンバー参照で一致が生成されない場合、結果がエラーである場合、または結果がメソッド グループを除くものである場合、エラーが生成され、それ以上の手順は実行されません。 - オーバーロードの解決は、空の引数リストを使用してメソッド グループに対して実行されます。 オーバーロードの解決によって該当するメソッドが得られない場合、あいまいさが生じるか、1 つの最適なメソッドになりますが、そのメソッドが静的であるかパブリックでないか、戻り値の型が
bool
に待機できない場合、エラーが生成され、それ以上の手順は実行されません。 - コレクション型が
X
、列挙子の型がE
、反復処理の型がCurrent
プロパティの型です。
- 識別子
- それ以外の場合は、列挙可能なインターフェイスを確認します。
X
からIAsyncEnumerable<ᵢ>
への暗黙的な変換があるTᵢ
すべての型の中に、T
が動的ではなく、他のすべてのTᵢ
に対してIAsyncEnumerable<T>
からIAsyncEnumerable<Tᵢ>
への暗黙的な変換が存在するように一意の型T
がある場合、コレクション型はインターフェイスIAsyncEnumerable<T>
であり、列挙子の型はインターフェイスIAsyncEnumerator<T>
です。 繰り返しの種類がT
。- それ以外の場合、このような型が複数存在する場合
T
、エラーが生成され、それ以上の手順は実行されません。
- それ以外の場合は、エラーが生成され、それ以上の手順は実行されません。
上記の手順が成功した場合は、コレクション型 C
、列挙子の型 E
、反復型 T
を明確に生成します。
await foreach (V v in x) «embedded_statement»
は次のように展開されます。
{
E e = ((C)(x)).GetAsyncEnumerator();
try {
while (await e.MoveNextAsync()) {
V v = (V)(T)e.Current;
«embedded_statement»
}
}
finally {
... // Dispose e
}
}
finally
ブロックの本体は、次の手順に従って構築されます。
- 型
E
に適切なDisposeAsync
メソッドがある場合:- 識別子
DisposeAsync
を持ち、型引数を持たない型E
に対してメンバー参照を実行します。 メンバー参照で一致が生成されない場合、またはあいまいさが生成された場合、またはメソッド グループではない一致が生成される場合は、次に示すように破棄インターフェイスを確認します。 - 結果のメソッド グループと空の引数リストを使用して、オーバーロードの解決を実行します。 オーバーロードの解決によって該当するメソッドが得られない場合、あいまいになる場合、または 1 つの最良の方法が得られますが、そのメソッドが静的またはパブリックでない場合は、次に説明するように破棄インターフェイスを確認します。
-
DisposeAsync
メソッドの戻り値型を待機できない場合は、エラーが生成され、それ以上の手順は実行されません。 finally
句は、次の意味に相当する形に拡張されます。
finally { await e.DisposeAsync(); }
- 識別子
- それ以外の場合、
E
からSystem.IAsyncDisposable
インターフェイスへの暗黙的な変換がある場合は、E
が null 非許容値型の場合、finally
句は次のセマンティックに相当するものに展開されます。
finally { await ((System.IAsyncDisposable)e).DisposeAsync(); }
- それ以外の場合、
finally
句は次のセマンティックに相当するものに拡張されます。
ただし、finally { System.IAsyncDisposable d = e as System.IAsyncDisposable; if (d != null) await d.DisposeAsync(); }
E
が値型または値型にインスタンス化された型パラメーターである場合、e
からSystem.IAsyncDisposable
への変換ではボックス化は行われません。
- それ以外の場合、
finally
句は空のブロックに展開されます。finally { }
ConfigureAwait
このパターンベースのコンパイルでは、ConfigureAwait
拡張メソッドを使用して、すべての await で ConfigureAwait
を使用できます。
await foreach (T item in enumerable.ConfigureAwait(false))
{
...
}
これもまた .NET に追加される型に基づいて行われ、System.Threading.Tasks.Extensions.dllになる可能性があります。
// Approximate implementation, omitting arg validation and the like
namespace System.Threading.Tasks
{
public static class AsyncEnumerableExtensions
{
public static ConfiguredAsyncEnumerable<T> ConfigureAwait<T>(this IAsyncEnumerable<T> enumerable, bool continueOnCapturedContext) =>
new ConfiguredAsyncEnumerable<T>(enumerable, continueOnCapturedContext);
public struct ConfiguredAsyncEnumerable<T>
{
private readonly IAsyncEnumerable<T> _enumerable;
private readonly bool _continueOnCapturedContext;
internal ConfiguredAsyncEnumerable(IAsyncEnumerable<T> enumerable, bool continueOnCapturedContext)
{
_enumerable = enumerable;
_continueOnCapturedContext = continueOnCapturedContext;
}
public ConfiguredAsyncEnumerator<T> GetAsyncEnumerator() =>
new ConfiguredAsyncEnumerator<T>(_enumerable.GetAsyncEnumerator(), _continueOnCapturedContext);
public struct ConfiguredAsyncEnumerator<T>
{
private readonly IAsyncEnumerator<T> _enumerator;
private readonly bool _continueOnCapturedContext;
internal ConfiguredAsyncEnumerator(IAsyncEnumerator<T> enumerator, bool continueOnCapturedContext)
{
_enumerator = enumerator;
_continueOnCapturedContext = continueOnCapturedContext;
}
public ConfiguredValueTaskAwaitable<bool> MoveNextAsync() =>
_enumerator.MoveNextAsync().ConfigureAwait(_continueOnCapturedContext);
public T Current => _enumerator.Current;
public ConfiguredValueTaskAwaitable DisposeAsync() =>
_enumerator.DisposeAsync().ConfigureAwait(_continueOnCapturedContext);
}
}
}
}
このアプローチでは、パターンベースの列挙子で ConfigureAwait
を使用することはできませんが、ConfigureAwait
が Task
/Task<T>
/ValueTask
/ValueTask<T>
の拡張機能としてのみ公開され、任意の待機可能なものに適用できないことは既に当てはまっていることに注意してください(タスクの継続サポートに実装された動作を制御します)。 したがって、待機可能なものがタスクではない可能性があるパターンを使用する場合は意味がありません。 待機可能なものを返すユーザーは、このような高度なシナリオで独自のカスタム動作を提供できます。
(スコープ レベルまたはアセンブリ レベルの ConfigureAwait
ソリューションをサポートする何らかの方法を考え出すことができる場合、これは必要ありません)。
非同期反復子
言語/コンパイラは、IAsyncEnumerable<T>
と IAsyncEnumerator<T>
の生成と消費をサポートします。 現在、言語は次のような反復子の記述をサポートしています。
static IEnumerable<int> MyIterator()
{
try
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(1000);
yield return i;
}
}
finally
{
Thread.Sleep(200);
Console.WriteLine("finally");
}
}
ただし、await
これらの反復子の本体では使用できません。 そのサポートを追加します。
構文
反復子に対する既存の言語サポートは、yield
が含まれているかどうかに基づいて、メソッドの反復子の性質を推論します。 非同期反復子についても同じことが当てはまります。 このような非同期反復子は、シグネチャに async
を追加することで同期反復子と区別され、戻り値の型として IAsyncEnumerable<T>
または IAsyncEnumerator<T>
も必要です。 たとえば、上記の例は、非同期反復子として次のように記述できます。
static async IAsyncEnumerable<int> MyIterator()
{
try
{
for (int i = 0; i < 100; i++)
{
await Task.Delay(1000);
yield return i;
}
}
finally
{
await Task.Delay(200);
Console.WriteLine("finally");
}
}
考えられる代替手段:
- 署名で
async
を使用しない:async
の使用は、コンパイラによって技術的に必要になる可能性があります。これは、そのコンテキストでawait
が有効かどうかを判断するために使用するためです。 ただし、必要ない場合でも、await
はasync
としてマークされたメソッドでのみ使用できることは確立されており、一貫性を保つことが重要であると思われます。 IAsyncEnumerable<T>
のカスタム ビルダーを有効にする: これは将来を見据えることができるものですが、機械は複雑であり、同期対応するユーザーにはサポートされていません。- シグネチャに
iterator
キーワードがある: 非同期反復子はシグネチャでasync iterator
を使用し、yield
はiterator
を含むasync
メソッドでのみ使用できます。iterator
は、同期反復子では省略可能になります。 あなたの視点に応じて、これは、コードがyield
を使用しているかどうかに基づいてコンパイラが型を製造するのではなく、yield
が許可されているかどうか、およびメソッドが実際に型IAsyncEnumerable<T>
のインスタンスを返すかどうかをメソッドのシグネチャによって非常に明確にする利点があります。 しかし、同期イテレーターはこれを必要とせず、それを必要とするようにすることはできません。 さらに、一部の開発者は余分な構文を気に入りません。 ゼロから設計する場合は、おそらくこれを必須にしますが、この時点で非同期反復子を同期反復子の近くに保つ方がはるかに多くの価値があります。
LINQ
System.Linq.Enumerable
クラスには約 200 個を超えるメソッドのオーバーロードがあり、そのすべてが IEnumerable<T>
の観点から機能します。これらの一部は IEnumerable<T>
を受け入れ、そのうちのいくつかは IEnumerable<T>
を生み出し、多くは両方を行います。 IAsyncEnumerable<T>
に LINQ サポートを追加するには、IAsyncEnumerable<T>
用にこれらのオーバーロードをすべて複製し、さらに約 200 個のオーバーロードを用意する必要があります。 また、IAsyncEnumerator<T>
は非同期世界のスタンドアロン エンティティとして、IEnumerator<T>
が同期世界よりも一般的である可能性が高いため、IAsyncEnumerator<T>
で動作する別の約 200 個のオーバーロードが必要になる可能性があります。 さらに、多くのオーバーロードは述語 (たとえば、Func<T, bool>
を受け取る Where
) を扱います。また、同期述語と非同期述語の両方を処理する IAsyncEnumerable<T>
ベースのオーバーロード (たとえば、Func<T, bool>
に加えて Func<T, ValueTask<bool>>
) を使用することが望ましい場合があります。 これは、現在の約 400 個の新しいオーバーロードすべてに適用できるわけではありませんが、大まかな計算では、もう 1 つの約 200 個のオーバーロードを意味する半分に適用され、合計で約 600 個の新しいメソッドが適用されます。
これは膨大な数の API であり、Interactive Extensions (Ix) などの拡張ライブラリが考慮される場合は、さらに多くの可能性があります。 しかし、Ixはすでにこれらの多くの実装を持っており、その作業を複製する大きな理由はないようです。代わりに、コミュニティで Ix の改善を支援し、開発者が linq と共に IAsyncEnumerable<T>
を使用する場合に推奨する必要があります。
クエリ理解構文の問題もあります。 クエリ理解のパターンベースの性質により、Ix が次のメソッドを提供する場合など、一部の演算子で "単に動作" できるようになります。
public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source, Func<TSource, TResult> func);
public static IAsyncEnumerable<T> Where(this IAsyncEnumerable<T> source, Func<T, bool> func);
その後、この C# コードは "単に動作" します。
IAsyncEnumerable<int> enumerable = ...;
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select item * 2;
ただし、句での await
の使用をサポートするクエリ理解構文はありません。Ix が追加された場合は、次のようになります。
public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source, Func<TSource, ValueTask<TResult>> func);
その後、これは問題なく動くでしょう。
IAsyncEnumerable<string> result = from url in urls
where item % 2 == 0
select SomeAsyncMethod(item);
async ValueTask<int> SomeAsyncMethod(int item)
{
await Task.Yield();
return item * 2;
}
しかし、await
句に select
をインラインで記述する方法はありません。 別の取り組みとして、async { ... }
式を言語に追加することを調べることができ、その時点でクエリの理解で使用できるようにすることができ、上記は代わりに次のように記述できます。
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select async
{
await Task.Yield();
return item * 2;
};
または、async from
をサポートするなどして、式で await
を直接使用できるようにします。 ただし、ここでの設計が機能セットの残りの部分に影響を与える可能性は低く、これは現在投資する価値が特に高いものではありません。そのため、今ここで追加の操作を行う必要はありません。
他の非同期フレームワークとの統合
IObservable<T>
やその他の非同期フレームワーク (リアクティブ ストリームなど) との統合は、言語レベルではなくライブラリ レベルで行われます。 たとえば、列挙子に IAsyncEnumerator<T>
し、データをオブザーバーに IObserver<T>
するだけで、await foreach
のすべてのデータを OnNext
に公開できるため、AsObservable<T>
拡張メソッドを使用することが可能になります。 await foreach
で IObservable<T>
を消費するには、前の項目がまだ処理中のときに別の項目がプッシュされた場合に備えてデータをバッファリングする必要がありますが、そのようなプッシュプルアダプターを簡単に実装することで、IAsyncEnumerator<T>
を使用して IObservable<T>
を取得することができます。 Rx/Ixは既にそのような実装のプロトタイプを提供しており、https://github.com/dotnet/corefx/tree/master/src/System.Threading.Channels のようなライブラリはさまざまな種類のバッファリングデータ構造を提供しています。 この段階では、言語を使用する必要はありません。
C# feature specifications