非同期ストリームAsync Streams
- [x] が提案されています[x] Proposed
- [x] プロトタイプ[x] Prototype
- [] の実装[ ] Implementation
- [] 仕様[ ] Specification
まとめSummary
C# では、反復子メソッドと非同期メソッドがサポートされていますが、反復子と非同期のメソッドの両方をサポートしていません。C# has support for iterator methods and async methods, but no support for a method that is both an iterator and an async method. 新しい形式の iterator でを使用できるようにすることで、これを修正する必要があり await
async
ます。この反復子は、またはではなくまたはを返し IAsyncEnumerable<T>
IAsyncEnumerator<T>
IEnumerable<T>
IEnumerator<T>
、 IAsyncEnumerable<T>
新しいで使用でき await foreach
ます。We should rectify this by allowing for await
to be used in a new form of async
iterator, one that returns an IAsyncEnumerable<T>
or IAsyncEnumerator<T>
rather than an IEnumerable<T>
or IEnumerator<T>
, with IAsyncEnumerable<T>
consumable in a new await foreach
. IAsyncDisposable
インターフェイスは、非同期のクリーンアップを有効にするためにも使用されます。An IAsyncDisposable
interface is also used to enable asynchronous cleanup.
関連の説明Related discussion
詳細なデザインDetailed design
インターフェイスInterfaces
IAsyncDisposableIAsyncDisposable
これについては、よく説明されています (例:)。 IAsyncDisposable
https://github.com/dotnet/roslyn/issues/114)There has been much discussion of IAsyncDisposable
(e.g. https://github.com/dotnet/roslyn/issues/114) and whether it's a good idea. ただし、非同期反復子のサポートを追加するために必要な概念です。However, it's a required concept to add in support of async iterators. finally
ブロックにはが含まれる場合があります。 await
また、ブロックは finally
反復子の破棄の一部として実行する必要があるため、非同期に破棄する必要があります。Since finally
blocks may contain await
s, and since finally
blocks need to be run as part of disposing of iterators, we need async disposal. また、一般に、リソースのクリーンアップでは、ファイルを閉じる (フラッシュが必要)、コールバックを解除する、登録解除が完了したことを知る方法を提供するなど、時間がかかる場合があります。It's also just generally useful any time cleaning up of resources might take any period of time, e.g. closing files (requiring flushes), deregistering callbacks and providing a way to know when deregistration has completed, etc.
次のインターフェイスがコア .NET ライブラリに追加されます (例: System.private.corelib/System)。The following interface is added to the core .NET libraries (e.g. System.Private.CoreLib / System.Runtime):
namespace System
{
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
}
と同様に、 Dispose
DisposeAsync
複数回の呼び出しが可能で、最初の後の後続の呼び出しは nops として処理され、同期的に完了したタスクを返す必要があり DisposeAsync
ます (ただし、スレッドセーフではなく、同時呼び出しをサポートする必要はありません)。As with Dispose
, invoking DisposeAsync
multiple times is acceptable, and subsequent invocations after the first should be treated as nops, returning a synchronously completed successful task (DisposeAsync
need not be thread-safe, though, and need not support concurrent invocation). さらに、型にはとの両方が実装されている場合がありますが、その場合はを呼び出すこともでき IDisposable
ますが、その逆も可能ですが、 IAsyncDisposable
Dispose
DisposeAsync
それ以降のいずれかの呼び出しが nop である必要があります。Further, types may implement both IDisposable
and IAsyncDisposable
, and if they do, it's similarly acceptable to invoke Dispose
and then DisposeAsync
or vice versa, but only the first should be meaningful and subsequent invocations of either should be a nop. そのため、型が両方を実装している場合、コンシューマーは、コンテキストに基づいて、同期コンテキストおよび非同期のメソッドを1回だけ呼び出すことをお勧めし Dispose
DisposeAsync
ます。As such, if a type does implement both, consumers are encouraged to call once and only once the more relevant method based on the context, Dispose
in synchronous contexts and DisposeAsync
in asynchronous ones.
( IAsyncDisposable
でのとの相互作用について説明 using
します。(I'm leaving discussion of how IAsyncDisposable
interacts with using
to a separate discussion. との相互作用について foreach
は、この提案で後ほど扱います)。And coverage of how it interacts with foreach
is handled later in this proposal.)
検討対象の候補:Alternatives considered:
DisposeAsync
を使用CancellationToken
すると、 理論的には、すべての非同期を取り消すことができます。破棄はクリーンアップ、終了、free'ing リソースなどです。これは通常、キャンセルする必要があるものではなく、キャンセルされた作業に対してクリーンアップは依然として重要です。DisposeAsync
accepting aCancellationToken
: while in theory it makes sense that anything async can be canceled, disposal is about cleanup, closing things out, free'ing resources, etc., which is generally not something that should be canceled; cleanup is still important for work that's canceled. 実際のCancellationToken
処理が取り消される原因となったものは、通常、に渡されるトークンと同じでありDisposeAsync
、DisposeAsync
作業の取り消しによって nop が発生する可能性があるため、意味がありDisposeAsync
ません。The sameCancellationToken
that caused the actual work to be canceled would typically be the same token passed toDisposeAsync
, makingDisposeAsync
worthless because cancellation of the work would causeDisposeAsync
to be a nop. 破棄を待機しているユーザーがブロックされないようにする場合は、結果ので待機しValueTask
たり、一定の期間だけ待機したりすることを避けることができます。If someone wants to avoid being blocked waiting for disposal, they can avoid waiting on the resultingValueTask
, or wait on it only for some period of time.DisposeAsync
をTask
返す: 非ジェネリックが存在し、から構築できるようになったため、からを返すことで、ValueTask
IValueTaskSource
ValueTask
DisposeAsync
既存のオブジェクトをの最終的な非同期完了を表す promise として再利用できるようになり、が非同期に完了したDisposeAsync
Task
場合に割り当てが保存さDisposeAsync
れるようになりました。DisposeAsync
returning aTask
: Now that a non-genericValueTask
exists and can be constructed from anIValueTaskSource
, returningValueTask
fromDisposeAsync
allows an existing object to be reused as the promise representing the eventual async completion ofDisposeAsync
, saving aTask
allocation in the case whereDisposeAsync
completes asynchronously.DisposeAsync
bool continueOnCapturedContext
(ConfigureAwait
) を使用したの構成: このような概念がusing
、foreach
、およびそれを使用する他の言語構成体に公開される方法に関連する問題が発生する場合がありますが、インターフェイスの観点からは、実際には何も実行されず、構成するものはありません.await
..のコンシューマーは、ValueTask
必要に応じてそれを使用できます。ConfiguringDisposeAsync
with abool continueOnCapturedContext
(ConfigureAwait
): While there may be issues related to how such a concept is exposed tousing
,foreach
, and other language constructs that consume this, from an interface perspective it's not actually doing anyawait
'ing and there's nothing to configure... consumers of theValueTask
can consume it however they wish.IAsyncDisposable
継承IDisposable
: 1 つだけを使用する必要があるため、型を強制的に実装することは意味がありません。IAsyncDisposable
inheritingIDisposable
: Since only one or the other should be used, it doesn't make sense to force types to implement both.- ではなく、"非同期的な処理" であるという名前の名前に従っている
IDisposableAsync
のIAsyncDisposable
に対し、操作は "非同期" になっています。そのため、型はプレフィックスとして "async" を持ち、メソッドはサフィックスとして "async" を持ちます。IDisposableAsync
instead ofIAsyncDisposable
: We've been following the naming that things/types are an "async something" whereas operations are "done async", so types have "Async" as a prefix and methods have "Async" as a suffix.
IAsyncEnumerable / IAsyncEnumeratorIAsyncEnumerable / IAsyncEnumerator
コア .NET ライブラリには、次の2つのインターフェイスが追加されます。Two interfaces are added to the core .NET libraries:
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; }
}
}
(追加の言語機能を使用しない) 一般的な消費は次のようになります。Typical consumption (without additional language features) would look like:
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
Use(enumerator.Current);
}
}
finally { await enumerator.DisposeAsync(); }
破棄されるオプション:Discarded options considered:
Task<bool> MoveNextAsync(); T current { get; }
: を使用Task<bool>
すると、キャッシュされたタスクオブジェクトを使用して同期的に成功したMoveNextAsync
呼び出しを表すことができますが、非同期の完了には割り当てが必要になります。Task<bool> MoveNextAsync(); T current { get; }
: UsingTask<bool>
would support using a cached task object to represent synchronous, successfulMoveNextAsync
calls, but an allocation would still be required for asynchronous completion. を返すことにより、ValueTask<bool>
列挙子オブジェクト自体を実装IValueTaskSource<bool>
し、から返されたのバッキングとして使用できるようになりますValueTask<bool>
MoveNextAsync
。これにより、オーバーヘッドを大幅に削減できます。By returningValueTask<bool>
, we enable the enumerator object to itself implementIValueTaskSource<bool>
and be used as the backing for theValueTask<bool>
returned fromMoveNextAsync
, which in turn allows for significantly reduced overheads.ValueTask<(bool, T)> MoveNextAsync();
: を使用するだけではなく、を共変にすることはできませんT
。ValueTask<(bool, T)> MoveNextAsync();
: It's not only harder to consume, but it means thatT
can no longer be covariant.ValueTask<T?> TryMoveNextAsync();
: 共変ではありません。ValueTask<T?> TryMoveNextAsync();
: Not covariant.Task<T?> TryMoveNextAsync();
: 共変ではなく、すべての呼び出しに対する割り当てなどです。Task<T?> TryMoveNextAsync();
: Not covariant, allocations on every call, etc.ITask<T?> TryMoveNextAsync();
: 共変ではなく、すべての呼び出しに対する割り当てなどです。ITask<T?> TryMoveNextAsync();
: Not covariant, allocations on every call, etc.ITask<(bool,T)> TryMoveNextAsync();
: 共変ではなく、すべての呼び出しに対する割り当てなどです。ITask<(bool,T)> TryMoveNextAsync();
: Not covariant, allocations on every call, etc.Task<bool> TryMoveNextAsync(out T result);
: 結果は、out
操作が同期的に返されたときに設定される必要があります。これは、今後長時間に及ぶ可能性があるタスクを非同期に完了するときではなく、結果を伝達する手段がないことを示します。Task<bool> TryMoveNextAsync(out T result);
: Theout
result would need to be set when the operation returns synchronously, not when it asynchronously completes the task potentially sometime long in the future, at which point there'd be no way to communicate the result.IAsyncEnumerator<T>
実装IAsyncDisposable
しない: これらを分離することができます。IAsyncEnumerator<T>
not implementingIAsyncDisposable
: We could choose to separate these. ただし、そのようにすることで、提案の他の特定の領域が複雑になります。これにより、コードは列挙子が破棄を提供しない可能性を処理できるようになるため、パターンベースのヘルパーを記述するのが困難になります。However, doing so complicates certain other areas of the proposal, as code must then be able to deal with the possibility that an enumerator doesn't provide disposal, which makes it difficult to write pattern-based helpers. さらに、列挙子が破棄を必要とすることがよくあります (たとえば、finally ブロックを持つ C# 非同期反復子、ほとんどの場合、ネットワーク接続からデータを列挙します)。それ以外の場合は、単純なpublic ValueTask DisposeAsync() => default(ValueTask);
オーバーヘッドを最小限に抑えて、メソッドを純粋に実装するのが簡単です。Further, it will be common for enumerators to have a need for disposal (e.g. any C# async iterator that has a finally block, most things enumerating data from a network connection, etc.), and if one doesn't, it is simple to implement the method purely aspublic ValueTask DisposeAsync() => default(ValueTask);
with minimal additional overhead.- _
IAsyncEnumerator<T> GetAsyncEnumerator()
: キャンセルトークンパラメーターがありません。_IAsyncEnumerator<T> GetAsyncEnumerator()
: No cancellation token parameter.
実行可能な代替手段:Viable alternative:
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つのインターフェイス呼び出しで項目を使用するために内側のループで使用されます。TryGetNext
is used in an inner loop to consume items with a single interface call as long as they're available synchronously. 次の項目を同期的に取得できない場合、false が返され、false が返された場合は、呼び出し元は WaitForNextAsync
次の項目が使用可能になるまで待機するか、別の項目がないことを確認するためにを呼び出す必要があります。When the next item can't be retrieved synchronously, it returns false, and any time it returns false, a caller must subsequently invoke WaitForNextAsync
to either wait for the next item to be available or to determine that there will never be another item. (追加の言語機能を使用しない) 一般的な消費は次のようになります。Typical consumption (without additional language features) would look like:
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つのフォールドと1つの重要な点です。The advantage of this is two-fold, one minor and one major:
- Minor: 列挙子が複数のコンシューマーをサポートできるように します。Minor: Allows for an enumerator to support multiple consumers. 複数の同時実行コンシューマーをサポートする列挙子にとって価値があるシナリオもあります。There may be scenarios where it's valuable for an enumerator to support multiple concurrent consumers. とが分離されているため、
MoveNextAsync
実装で使用できないようにすることはできませんCurrent
。That can't be achieved whenMoveNextAsync
andCurrent
are separate such that an implementation can't make their usage atomic. これに対して、この方法では、TryGetNext
列挙子を前方にプッシュして次の項目を取得することをサポートする1つのメソッドが提供されるため、必要に応じて列挙子を有効にすることができます。In contrast, this approach provides a single methodTryGetNext
that supports pushing the enumerator forward and getting the next item, so the enumerator can enable atomicity if desired. ただし、このようなシナリオは、各コンシューマーに、共有されている列挙可能な独自の列挙子を与えることで有効にすることもできます。However, it's likely that such scenarios could also be enabled by giving each consumer its own enumerator from a shared enumerable. さらに、すべての列挙子が同時使用をサポートしないようにすることをお勧めします。これにより、ほとんどの場合、これを必要としない大部分のオーバーヘッドが発生します。つまり、インターフェイスのコンシューマーは一般にこのような方法に依存することはできません。Further, we don't want to enforce that every enumerator support concurrent usage, as that would add non-trivial overheads to the majority case that doesn't require it, which means a consumer of the interface generally couldn't rely on this any way. - Major: パフォーマンス。Major: Performance. このアプローチでは、
MoveNextAsync
/Current
操作ごとに2つのインターフェイス呼び出しが必要ですが、の最適なケースは、WaitForNextAsync
/TryGetNext
ほとんどの反復処理が同期的に完了し、TryGetNext
操作ごとに1つのインターフェイス呼び出しのみが有効になるように、を使用して厳密な内側ループを有効にすることです。TheMoveNextAsync
/Current
approach requires two interface calls per operation, whereas the best case forWaitForNextAsync
/TryGetNext
is that most iterations complete synchronously, enabling a tight inner loop withTryGetNext
, such that we only have one interface call per operation. これは、インターフェイス呼び出しが計算を独占する場合に、大きな影響を与える可能性があります。This can have a measurable impact in situations where the interface calls dominate the computation.
ただし、これらを手動で使用する場合の複雑さが大幅に増加し、それらを使用したときにバグが発生する可能性が高くなるなど、重要ではない欠点があります。However, there are non-trivial downsides, including significantly increased complexity when consuming these manually, and an increased chance of introducing bugs when using them. また、パフォーマンス上の利点はマイクロベンチマークにも反映されていますが、実際の使用の大部分にインパクトされるとは思えません。And while the performance benefits show up in microbenchmarks, we don't believe they'll be impactful in the vast majority of real usage. そのようなことが判明した場合は、2番目のインターフェイスセットを軽量な方法で導入できます。If it turns out they are, we can introduce a second set of interfaces in a light-up fashion.
破棄されるオプション:Discarded options considered:
ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result);
:out
パラメーターを共変にすることはできません。ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result);
:out
parameters can't be covariant. ここでは、参照型の結果に対してランタイム書き込みバリアが発生する可能性があるという小さな影響もあります (一般的な try パターンの問題)。There's also a small impact here (an issue with the try pattern in general) that this likely incurs a runtime write barrier for reference type results.
キャンセルCancellation
キャンセルをサポートするには、いくつかの方法が考えられます。There are several possible approaches to supporting cancellation:
IAsyncEnumerable<T>
/IAsyncEnumerator<T>
取り消しにとらわれない:CancellationToken
どこにも表示されません。IAsyncEnumerable<T>
/IAsyncEnumerator<T>
are cancellation-agnostic:CancellationToken
doesn't appear anywhere. 取り消しを行うには、をCancellationToken
任意の方法で列挙型または列挙子に論理的に加算します。たとえば、反復子を呼び出す場合は、をCancellationToken
引数として iterator メソッドに渡し、反復子の本体でその他のパラメーターを使用します。Cancellation is achieved by logically baking theCancellationToken
into the enumerable and/or enumerator in whatever manner is appropriate, e.g. when calling an iterator, passing theCancellationToken
as an argument to the iterator method and using it in the body of the iterator, as is done with any other parameter.IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken)
:CancellationToken
をに渡しGetAsyncEnumerator
ます。後続のMoveNextAsync
操作は可能です。IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken)
: You pass aCancellationToken
toGetAsyncEnumerator
, and subsequentMoveNextAsync
operations respect it however it can.IAsyncEnumerator<T>.MoveNextAsync(CancellationToken)
: を個々のCancellationToken
呼び出しに渡しMoveNextAsync
ます。IAsyncEnumerator<T>.MoveNextAsync(CancellationToken)
: You pass aCancellationToken
to each individualMoveNextAsync
call.- 1 && 2: は、列挙可能な
CancellationToken
/列挙子に s を埋め込み、CancellationToken
s をに渡しGetAsyncEnumerator
ます。1 && 2: You both embedCancellationToken
s into your enumerable/enumerator and passCancellationToken
s intoGetAsyncEnumerator
. - 1 && 3: 列挙可能な
CancellationToken
/列挙子に s を埋め込み、CancellationToken
s をに渡しMoveNextAsync
ます。1 && 3: You both embedCancellationToken
s into your enumerable/enumerator and passCancellationToken
s intoMoveNextAsync
.
純粋な理論上の観点からは、(5) が最も堅牢であり、(a) を受け入れることで、取り消された MoveNextAsync
CancellationToken
ものを最も細かく制御できるようになりました。 (b) CancellationToken
は、反復子に引数として渡すことができる他の型であり、任意の型に埋め込むことができます。From a purely theoretical perspective, (5) is the most robust, in that (a) MoveNextAsync
accepting a CancellationToken
enables the most fine-grained control over what's canceled, and (b) CancellationToken
is just any other type that can passed as an argument into iterators, embedded in arbitrary types, etc.
ただし、この方法には複数の問題があります。However, there are multiple problems with that approach:
- は、
CancellationToken
をGetAsyncEnumerator
反復子の本体に渡すためにどのように渡されますか。How does aCancellationToken
passed toGetAsyncEnumerator
make it into the body of the iterator?iterator
に渡されたへのアクセスを取得するために、からドットを切ることができる新しいキーワードを公開できCancellationToken
ます。GetEnumerator
しかし、a) これは多くの追加メカニズムです。 b) これを非常にファーストクラスの市民にします。 c) 99% のケースは、反復子を呼び出して呼び出しを行うのと同じコードであるように見えGetAsyncEnumerator
ます。この場合、をCancellationToken
引数としてメソッドに渡すだけで済みます。We could expose a newiterator
keyword that you could dot off of to get access to theCancellationToken
passed toGetEnumerator
, but a) that's a lot of additional machinery, b) we're making it a very first-class citizen, and c) the 99% case would seem to be the same code both calling an iterator and callingGetAsyncEnumerator
on it, in which case it can just pass theCancellationToken
as an argument into the method. - が
CancellationToken
メソッドの本体に渡される方法を教えてMoveNextAsync
ください。How does aCancellationToken
passed toMoveNextAsync
get into the body of the method? これはさらに悪化します。ローカルオブジェクトから公開されているかのように、iterator
その値は待機中に変更される可能性があります。したがって、トークンに登録されているすべてのコードは、を待機してから再登録する必要があります。これは、MoveNextAsync
反復子または開発者が手動で実装したかどうかに関係なく、すべての呼び出しThis is even worse, as if it's exposed off of aniterator
local object, its value could change across awaits, which means any code that registered with the token would need to unregister from it prior to awaits and then re-register after; it's also potentially quite expensive to need to do such registering and unregistering in everyMoveNextAsync
call, regardless of whether implemented by the compiler in an iterator or by a developer manually. - 開発者はどのようにしてループをキャンセルし
foreach
ますか。How does a developer cancel aforeach
loop? を列挙可能な/列挙子に渡すことによって完了した場合は、列挙CancellationToken
子に対する ' ing ' をサポートする必要があります。これにより、ファーストクラスの市民であることがわかります。foreach
次に、列挙子 (LINQ メソッドなど) または b) の前後に構築されたエコシステムについて考える必要があります。その場合、指定したCancellationToken
WithCancellation
トークンを格納するの拡張メソッドをから除外し、返された構造体のが呼び出されたときに、ラップされた列挙可能なにIAsyncEnumerable<T>
渡しGetAsyncEnumerator
GetAsyncEnumerator
ます。If it's done by giving aCancellationToken
to an enumerable/enumerator, then either a) we need to supportforeach
'ing over enumerators, which raises them to being first-class citizens, and now you need to start thinking about an ecosystem built up around enumerators (e.g. LINQ methods) or b) we need to embed theCancellationToken
in the enumerable anyway by having someWithCancellation
extension method off ofIAsyncEnumerable<T>
that would store the provided token and then pass it into the wrapped enumerable'sGetAsyncEnumerator
when theGetAsyncEnumerator
on the returned struct is invoked (ignoring that token). または、CancellationToken
foreach の本体にあるを使用することもできます。Or, you can just use theCancellationToken
you have in the body of the foreach. - Query の包含がサポートされている場合、
CancellationToken
GetEnumerator
または各句に渡されるのはどのようになりMoveNextAsync
ますか。If/when query comprehensions are supported, how would theCancellationToken
supplied toGetEnumerator
orMoveNextAsync
be passed into each clause? 最も簡単な方法は、句をキャプチャすることだけです。この時点で、に渡されるトークンは無視されGetAsyncEnumerator
/MoveNextAsync
ます。The easiest way would simply be for the clause to capture it, at which point whatever token is passed toGetAsyncEnumerator
/MoveNextAsync
is ignored.
このドキュメントの以前のバージョンでは、(1) が推奨されていましたが、(4) に切り替えました。An earlier version of this document recommended (1), but we since switched to (4).
(1) の主な問題は次の2つです。The two main problems with (1):
- キャンセル可能な列挙体のプロデューサーは、いくつかの定型を実装する必要があり、コンパイラによる非同期反復子のサポートのみを利用してメソッドを実装でき
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken)
ます。producers of cancellable enumerables have to implement some boilerplate, and can only leverage the compiler's support for async-iterators to implement aIAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken)
method. - 多くのプロデューサーは、代わりに、非同期の列挙可能なシグネチャにパラメーターを追加することを考えています
CancellationToken
。これにより、コンシューマーが型を指定したときに必要なキャンセルトークンを渡すことができなくなりますIAsyncEnumerable
。it is likely that many producers would be tempted to just add aCancellationToken
parameter to their async-enumerable signature instead, which will prevent consumers from passing the cancellation token they want when they are given anIAsyncEnumerable
type.
主に次の2つのシナリオがあります。There are two main consumption scenarios:
await foreach (var i in GetData(token)) ...
コンシューマーが非同期反復子メソッドを呼び出す場所await foreach (var i in GetData(token)) ...
where the consumer calls the async-iterator method,await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ...
コンシューマーが特定のインスタンスを処理しIAsyncEnumerable
ます。await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ...
where the consumer deals with a givenIAsyncEnumerable
instance.
非同期ストリームのプロデューサーとコンシューマーの両方にとって便利な方法で両方のシナリオをサポートするための適切な妥協点は、async iterator メソッドで特別に注釈が付けられたパラメーターを使用することです。We find that a reasonable compromise to support both scenarios in a way that is convenient for both producers and consumers of async-streams is to use a specially annotated parameter in the async-iterator method. [EnumeratorCancellation]
属性は、この目的で使用されます。The [EnumeratorCancellation]
attribute is used for this purpose. この属性をパラメーターに設定すると、トークンがメソッドに渡される場合に、 GetAsyncEnumerator
パラメーターに最初に渡された値の代わりにそのトークンを使用する必要があることがコンパイラに通知されます。Placing this attribute on a parameter tells the compiler that if a token is passed to the GetAsyncEnumerator
method, that token should be used instead of the value originally passed for the parameter.
IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default)
を検討します。Consider IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default)
. このメソッドの実装者は、メソッドの本体でパラメーターを使用するだけで済みます。The implementer of this method can simply use the parameter in the method body. コンシューマーは、上記のいずれかの消費パターンを使用できます。The consumer can use either consumption patterns above:
- を使用する
GetData(token)
と、トークンは非同期の列挙型に保存され、反復処理で使用されます。if you useGetData(token)
, then the token is saved into the async-enumerable and will be used in iteration, - を使用する場合、
givenIAsyncEnumerable.WithCancellation(token)
に渡されるトークンは、GetAsyncEnumerator
非同期の列挙可能なトークンに置き換えられます。if you usegivenIAsyncEnumerable.WithCancellation(token)
, then the token passed toGetAsyncEnumerator
will supersede any token saved in the async-enumerable.
foreachforeach
foreach
は、の既存のサポートに加えて、をサポートするように強化され IAsyncEnumerable<T>
IEnumerable<T>
ます。foreach
will be augmented to support IAsyncEnumerable<T>
in addition to its existing support for IEnumerable<T>
. また、関連するメンバーがパブリックに公開され、そうでない場合はインターフェイスを直接使用するようにフォールバックして、の戻り値の型として代替の awaitables 使用できるようにするために、のパターンと同等のをサポートし IAsyncEnumerable<T>
MoveNextAsync
DisposeAsync
ます。And it will support the equivalent of IAsyncEnumerable<T>
as a pattern if the relevant members are exposed publicly, falling back to using the interface directly if not, in order to enable struct-based extensions that avoid allocating as well as using alternative awaitables as the return type of MoveNextAsync
and DisposeAsync
.
構文Syntax
使用する構文は です。Using the syntax:
foreach (var i in enumerable)
C# は、 enumerable
同期的な列挙型として処理を続行します。これにより、非同期列挙体 (パターンの公開またはインターフェイスの実装) に関連する api を公開する場合でも、同期 api のみが考慮されます。C# will continue to treat enumerable
as a synchronous enumerable, such that even if it exposes the relevant APIs for async enumerables (exposing the pattern or implementing the interface), it will only consider the synchronous APIs.
強制的 foreach
に非同期 api のみを考慮するようにを強制するために、は次のように await
挿入されます。To force foreach
to instead only consider the asynchronous APIs, await
is inserted as follows:
await foreach (var i in enumerable)
非同期 Api または同期 Api の使用をサポートする構文は提供されません。開発者は、使用する構文に基づいて選択する必要があります。No syntax would be provided that would support using either the async or the sync APIs; the developer must choose based on the syntax used.
破棄されるオプション:Discarded options considered:
foreach (var i in await enumerable)
: これは既に有効な構文です。その意味を変更すると、互換性に影響する変更になります。foreach (var i in await enumerable)
: This is already valid syntax, and changing its meaning would be a breaking change. これは、に対してawait
同期的に反復可能なを取得し、その結果を同期的に反復処理することを意味しenumerable
ます。This means toawait
theenumerable
, get back something synchronously iterable from it, and then synchronously iterate through that.foreach (var i await in enumerable)
、foreach (var await i in enumerable)
、foreach (await var i in enumerable)
: これらは、次の項目を待機していることを示唆していますが、foreach には他の待機が含まれています。特に、列挙型の場合は、非同期の破棄を行うことになりIAsyncDisposable
await
ます。foreach (var i await in enumerable)
,foreach (var await i in enumerable)
,foreach (await var i in enumerable)
: These all suggest that we're awaiting the next item, but there are other awaits involved in foreach, in particular if the enumerable is anIAsyncDisposable
, we will beawait
'ing its async disposal. その await は、個々の要素に対してではなく foreach のスコープとなります。したがって、await
キーワードはレベルで指定することができforeach
ます。That await is as the scope of the foreach rather than for each individual element, and thus theawait
keyword deserves to be at theforeach
level. さらに、に関連付けられている場合は、foreach
foreach
"await foreach" など、別の用語でを記述する方法が提供されます。Further, having it associated with theforeach
gives us a way to describe theforeach
with a different term, e.g. a "await foreach". しかし、さらに重要なのは、構文をforeach
構文と同じusing
ように使用して相互に一貫性を保ち、using (await ...)
既に有効な構文であるということです。But more importantly, there's value in consideringforeach
syntax at the same time asusing
syntax, so that they remain consistent with each other, andusing (await ...)
is already valid syntax.foreach await (var i in enumerable)
引き続き検討してください。Still to consider:
foreach
現在、列挙子の反復処理はサポートされていません。foreach
today does not support iterating through an enumerator. を使用する方が一般的であると考えられIAsyncEnumerator<T>
ます。したがって、との両方でサポートが必要になりawait foreach
IAsyncEnumerable<T>
IAsyncEnumerator<T>
ます。We expect it will be more common to haveIAsyncEnumerator<T>
s handed around, and thus it's tempting to supportawait foreach
with bothIAsyncEnumerable<T>
andIAsyncEnumerator<T>
. しかし、このようなサポートを追加すると、がファーストクラスの市民であるかどうか、IAsyncEnumerator<T>
および列挙体に加えて列挙子を操作する連結子のオーバーロードが必要かどうかという疑問が生じます。But once we add such support, it introduces the question of whetherIAsyncEnumerator<T>
is a first-class citizen, and whether we need to have overloads of combinators that operate on enumerators in addition to enumerables? 列挙体ではなく列挙子を返すメソッドを推奨しますか。Do we want to encourage methods to return enumerators rather than enumerables? この点については、引き続き説明します。We should continue to discuss this. サポートしないと判断した場合は、public static IAsyncEnumerable<T> AsEnumerable<T>(this IAsyncEnumerator<T> enumerator);
列挙子を引き続き使用できるようにする拡張メソッドを導入することをお勧めしforeach
ます。If we decide we don't want to support it, we might want to introduce an extension methodpublic static IAsyncEnumerable<T> AsEnumerable<T>(this IAsyncEnumerator<T> enumerator);
that would allow an enumerator to still beforeach
'd. サポートを希望する場合は、が列挙子に対してを呼び出す必要があるかどうかを判断する必要があります。また、そのawait foreach
DisposeAsync
答えは、"いいえ、破棄の制御はだれが行ったかによって処理する必要がある" と考えられGetEnumerator
ます。If we decide we do want to support it, we'll need to also decide on whether theawait foreach
would be responsible for callingDisposeAsync
on the enumerator, and the answer is likely "no, control over disposal should be handled by whoever calledGetEnumerator
."
パターンに基づくコンパイルPattern-based Compilation
コンパイラは、存在する場合はパターンベースの Api にバインドし、インターフェイスを使用してこれらを優先します (パターンはインスタンスメソッドまたは拡張メソッドで満たされる場合があります)。The compiler will bind to the pattern-based APIs if they exist, preferring those over using the interface (the pattern may be satisfied with instance methods or extension methods). パターンの要件は次のとおりです。The requirements for the pattern are:
- 列挙可能なは、
GetAsyncEnumerator
引数なしで呼び出すことができ、関連するパターンを満たす列挙子を返すメソッドを公開する必要があります。The enumerable must expose aGetAsyncEnumerator
method that may be called with no arguments and that returns an enumerator that meets the relevant pattern. - 列挙子は、引数なしで呼び出される可能性があるメソッドを公開する必要があり
MoveNextAsync
ます。このメソッドは、ed であり、がを返す場合があるものを返しawait
GetResult()
bool
ます。The enumerator must expose aMoveNextAsync
method that may be called with no arguments and that returns something which may beawait
ed and whoseGetResult()
returns abool
. - 列挙子は、
Current
T
列挙されるデータの種類を表すを返す getter を持つプロパティも公開する必要があります。The enumerator must also exposeCurrent
property whose getter returns aT
representing the kind of data being enumerated. - 列挙子は、必要に応じて、引数を指定せずに呼び出すことができるメソッドを公開することがあり
DisposeAsync
ます。このメソッドは、await
ed と戻り値を返すことができるものを返しGetResult()
void
ます。The enumerator may optionally expose aDisposeAsync
method that may be invoked with no arguments and that returns something that can beawait
ed and whoseGetResult()
returnsvoid
.
このコードによって以下が行われます。This code:
var enumerable = ...;
await foreach (T item in enumerable)
{
...
}
はに相当するものに変換されます。is translated to the equivalent of:
var enumerable = ...;
var enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
T item = enumerator.Current;
...
}
}
finally
{
await enumerator.DisposeAsync(); // omitted, along with the try/finally, if the enumerator doesn't expose DisposeAsync
}
反復される型が適切パターンを公開していない場合は、インターフェイスが使用されます。If the iterated type doesn't expose the right pattern, the interfaces will be used.
ConfigureAwaitConfigureAwait
このパターンに基づくコンパイルでは、拡張メソッドを使用して、 ConfigureAwait
すべての待機でを使用でき ConfigureAwait
ます。This pattern-based compilation will allow ConfigureAwait
to be used on all of the awaits, via a ConfigureAwait
extension method:
await foreach (T item in enumerable.ConfigureAwait(false))
{
...
}
これは .NET に追加する型に基づいており、System.Threading.Tasks.Extensions.dll 可能性があります。This will be based on types we'll add to .NET as well, likely to 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 Enumerator
{
private readonly IAsyncEnumerator<T> _enumerator;
private readonly bool _continueOnCapturedContext;
internal Enumerator(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>
任意の待機可能には適用できません。これは、タスク (タスクの継続サポートに実装されている動作を制御します) に適用した場合にのみ意味があるため、待機可能がタスクでない可能性があるパターンを使用する場合は意味がありません。Note that this approach will not enable ConfigureAwait
to be used with pattern-based enumerables, but then again it's already the case that the ConfigureAwait
is only exposed as an extension on Task
/Task<T>
/ValueTask
/ValueTask<T>
and can't be applied to arbitrary awaitable things, as it only makes sense when applied to Tasks (it controls a behavior implemented in Task's continuation support), and thus doesn't make sense when using a pattern where the awaitable things may not be tasks. 待機可能を返す人は、このような高度なシナリオで独自のカスタム動作を提供できます。Anyone returning awaitable things can provide their own custom behavior in such advanced scenarios.
(スコープまたはアセンブリレベルのソリューションをサポートする何らかの方法を使用できる場合は ConfigureAwait
、これは必要ありません)。(If we can come up with some way to support a scope- or assembly-level ConfigureAwait
solution, then this won't be necessary.)
非同期反復子Async Iterators
言語/コンパイラは、を使用 IAsyncEnumerable<T>
することに加えて、との生成 IAsyncEnumerator<T>
をサポートします。The language / compiler will support producing IAsyncEnumerable<T>
s and IAsyncEnumerator<T>
s in addition to consuming them. 現在、この言語では次のような反復子の作成がサポートされています。Today the language supports writing an iterator like:
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
、これらの反復子の本体では使用できません。but await
can't be used in the body of these iterators. サポートを追加します。We will add that support.
構文Syntax
反復子に対する既存の言語サポートは、が含まれているかどうかに基づいて、メソッドの反復子の性質を推論し yield
ます。The existing language support for iterators infers the iterator nature of the method based on whether it contains any yield
s. 非同期反復子についても同じことが当てはまります。The same will be true for async iterators. このような非同期反復子は、署名に追加することによって同期反復子と区別され、 async
戻り値の型としてまたはのいずれかを持つ必要があり IAsyncEnumerable<T>
IAsyncEnumerator<T>
ます。 demarcated です。Such async iterators will be demarcated and differentiated from synchronous iterators via adding async
to the signature, and must then also have either IAsyncEnumerable<T>
or IAsyncEnumerator<T>
as its return type. たとえば、上の例は、次のように、非同期反復子として記述できます。For example, the above example could be written as an async iterator as follows:
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");
}
}
検討対象の候補:Alternatives considered:
async
シグネチャでを使用しない: を使用async
すると、コンパイラは、そのawait
コンテキストでが有効かどうかを判断するために使用します。これは、コンパイラが技術的に必要とすることです。Not usingasync
in the signature: Usingasync
is likely technically required by the compiler, as it uses it to determine whetherawait
is valid in that context. ただし、必須ではありませんが、await
としてマークされたメソッドでのみ使用できるように設定されていasync
ます。一貫性を保つことが重要に見えます。But even if it's not required, we've established thatawait
may only be used in methods marked asasync
, and it seems important to keep the consistency.- の
IAsyncEnumerable<T>
カスタムビルダーを有効 にすると、将来のために参照できますが、機械は複雑になり、対応する同期に対してはサポートされません。Enabling custom builders forIAsyncEnumerable<T>
: That's something we could look at for the future, but the machinery is complicated and we don't support that for the synchronous counterparts. iterator
シグネチャにキーワードを 指定すると、非同期反復子はシグネチャでを使用します。これは、がasync iterator
yield
含まれているメソッドでのみ使用できます。async
iterator
iterator
その後、同期反復子に対してオプションになります。Having aniterator
keyword in the signature: Async iterators would useasync iterator
in the signature, andyield
could only be used inasync
methods that includediterator
;iterator
would then be made optional on synchronous iterators. パースペクティブによっては、が許可されているかどうかにかかわらず、メソッドのシグネチャによって、メソッドのシグネチャによって、コードがを使用しているかどうかyield
に基づいて、コンパイラの製造ではなく型のインスタンスを実際に返すことができるという利点がありIAsyncEnumerable<T>
yield
ます。Depending on your perspective, this has the benefit of making it very clear by the signature of the method whetheryield
is allowed and whether the method is actually meant to return instances of typeIAsyncEnumerable<T>
rather than the compiler manufacturing one based on whether the code usesyield
or not. ただし、これは同期反復子とは異なり、要求を要求することはできません。But it is different from synchronous iterators, which don't and can't be made to require one. さらに、開発者によっては、余分な構文が気に入らない場合もあります。Plus some developers don't like the extra syntax. 最初からデザインする場合は、これが必要になることがありますが、この時点で、非同期反復子を同期反復子の近くに維持することには、より多くの価値があります。If we were designing it from scratch, we'd probably make this required, but at this point there's much more value in keeping async iterators close to sync iterators.
LINQLINQ
クラスには ~ 200 のオーバーロードがあります System.Linq.Enumerable
。これらのメソッドはすべて、という観点で動作します。これらのうちのいくつかは、その一部を生成し、その IEnumerable<T>
両方を生成し IEnumerable<T>
IEnumerable<T>
ます。There are over ~200 overloads of methods on the System.Linq.Enumerable
class, all of which work in terms of IEnumerable<T>
; some of these accept IEnumerable<T>
, some of them produce IEnumerable<T>
, and many do both. LINQ サポートをに追加する IAsyncEnumerable<T>
ことは、そのようなオーバーロードをすべて複製することが必要になる可能性があります。 ~ 200 です。Adding LINQ support for IAsyncEnumerable<T>
would likely entail duplicating all of these overloads for it, for another ~200. また、 IAsyncEnumerator<T>
は、同期の世界中の場合よりも、非同期環境ではスタンドアロンのエンティティとして一般的である可能性が高いため IEnumerator<T>
、で使用できる別の ~ 200 のオーバーロードが必要になる可能性があり IAsyncEnumerator<T>
ます。And since IAsyncEnumerator<T>
is likely to be more common as a standalone entity in the asynchronous world than IEnumerator<T>
is in the synchronous world, we could potentially need another ~200 overloads that work with IAsyncEnumerator<T>
. さらに、多くのオーバーロードは述語を処理します (たとえば Where
、を受け取ります Func<T, bool>
)。また、 IAsyncEnumerable<T>
同期述語と非同期述語の両方を処理するベースのオーバーロードを使用することをお勧めします (など Func<T, ValueTask<bool>>
Func<T, bool>
)。Plus, a large number of the overloads deal with predicates (e.g. Where
that takes a Func<T, bool>
), and it may be desirable to have IAsyncEnumerable<T>
-based overloads that deal with both synchronous and asynchronous predicates (e.g. Func<T, ValueTask<bool>>
in addition to Func<T, bool>
). これは、現在 ~ 400 の新しいオーバーロードには適用されませんが、大まかな計算として、半分に適用できることが挙げられます。これは、合計で600の新しいメソッドに対して、別の ~ 200 のオーバーロードを意味します。While this isn't applicable to all of the now ~400 new overloads, a rough calculation is that it'd be applicable to half, which means another ~200 overloads, for a total of ~600 new methods.
これは、対話的な拡張機能 (Ix) のような拡張ライブラリが検討されている場合に、さらに多くの Api を使用できるようになります。That is a staggering number of APIs, with the potential for even more when extension libraries like Interactive Extensions (Ix) are considered. ただし、Ix にはこれらの多くの実装が既に存在しているので、その作業を複製するのに大きな理由があるとは思えません。代わりに、開発者がを使用して LINQ を使用する場合には、このコミュニティが Ix を改善し、推奨されることをお勧めし IAsyncEnumerable<T>
ます。But Ix already has an implementation of many of these, and there doesn't seem to be a great reason to duplicate that work; we should instead help the community improve Ix and recommend it for when developers want to use LINQ with IAsyncEnumerable<T>
.
クエリの読解構文にも問題があります。There is also the issue of query comprehension syntax. クエリの包含のパターンベースの性質により、いくつかの演算子を使用することができます。たとえば、Ix に次のようなメソッドが用意されているとします。The pattern-based nature of query comprehensions would allow them to "just work" with some operators, e.g. if Ix provides the following methods:
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# コードは "作業のみ" になります。then this C# code will "just work":
IAsyncEnumerable<int> enumerable = ...;
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select item * 2;
ただし、次の例のように、句でのの使用をサポートするクエリの読解構文はありません。そのため、Ix を追加した場合は、 await
次のようになります。However, there is no query comprehension syntax that supports using await
in the clauses, so if Ix added, for example:
public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source, Func<TSource, ValueTask<TResult>> func);
次に、これは "仕事のみ" になります。then this would "just work":
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
。but there'd be no way to write it with the await
inline in the select
clause. 別の作業として、言語に式を追加することもできます async { ... }
。その時点で、クエリの包含での使用を許可できます。上記の例は、次のように記述できます。As a separate effort, we could look into adding async { ... }
expressions to the language, at which point we could allow them to be used in query comprehensions and the above could instead be written as:
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select async
{
await Task.Yield();
return item * 2;
};
をサポートするなどして await
、を式で直接使用できるようにする場合は async from
。or to enabling await
to be used directly in expressions, such as by supporting async from
. ただし、ここでは、機能セットの他の部分に影響を与える可能性はほとんどありません。これは、現時点では特に投資が重要な価値の高いものではないため、ここでは何も追加しないことを提案します。However, it's unlikely a design here would impact the rest of the feature set one way or the other, and this isn't a particularly high-value thing to invest in right now, so the proposal is to do nothing additional here right now.
他の非同期フレームワークとの統合Integration with other asynchronous frameworks
IObservable<T>
およびその他の非同期フレームワーク (リアクティブストリームなど) との統合は、言語レベルではなくライブラリレベルで実行されます。Integration with IObservable<T>
and other asynchronous frameworks (e.g. reactive streams) would be done at the library level rather than at the language level. たとえば、のすべてのデータを IAsyncEnumerator<T>
IObserver<T>
await foreach
"列挙子に対して" と OnNext
"データをオブザーバーに" ing "するだけでに公開できるため、 AsObservable<T>
拡張メソッドを使用できます。For example, all of the data from an IAsyncEnumerator<T>
can be published to an IObserver<T>
simply by await foreach
'ing over the enumerator and OnNext
'ing the data to the observer, so an AsObservable<T>
extension method is possible. でを使用するには、 IObservable<T>
await foreach
データのバッファリングが必要です (前の項目がまだ処理されている間に別の項目がプッシュされた場合)。ただし、このようなプッシュプルアダプターは、を使用してをプルできるように簡単に実装でき IObservable<T>
IAsyncEnumerator<T>
ます。Consuming an IObservable<T>
in a await foreach
requires buffering the data (in case another item is pushed while the previous item is still being processing), but such a push-pull adapter can easily be implemented to enable an IObservable<T>
to be pulled from with an IAsyncEnumerator<T>
. 等. Rx/Ix は、このような実装のプロトタイプを既に提供しています。また、などのライブラリは、 https://github.com/dotnet/corefx/tree/master/src/System.Threading.Channels さまざまな種類のバッファリングデータ構造を提供しています。Etc. Rx/Ix already provide prototypes of such implementations, and libraries like https://github.com/dotnet/corefx/tree/master/src/System.Threading.Channels provide various kinds of buffering data structures. この段階では、言語を使用する必要はありません。The language need not be involved at this stage.