次の方法で共有


非同期ストリーム

手記

この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。

機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。

機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。

概要

C# では反復子メソッドと非同期メソッドがサポートされていますが、反復子と非同期メソッドの両方であるメソッドはサポートされません。 これを修正するには、await を新しい形式の async 反復子として使用し、IEnumerable<T>IEnumerator<T>の代わりに IAsyncEnumerable<T> または IAsyncEnumerator<T> を返す形にし、新しい await foreachIAsyncEnumerable<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 スレッド セーフである必要はなく、同時呼び出しをサポートする必要もありません)。 さらに、型は IDisposableIAsyncDisposableの両方を実装できます。その場合、Dispose を呼び出してから DisposeAsync またはその逆を呼び出すのと同様に受け入れられますが、最初の呼び出しだけが意味があり、それ以降の呼び出しのみが nop である必要があります。 そのため、型が両方を実装する場合、コンシューマーは、コンテキストに基づいてより関連性の高いメソッドを 1 回だけ呼び出し、同期コンテキストで Dispose し、非同期メソッドで DisposeAsync することをお勧めします。

(IAsyncDisposableusing とやり取りする方法は別の説明です。また、foreach との対話方法については、この提案の後半で取り扱います)。

考えられる代替手段:

  • を受け入れる :理論的には非同期的なものは取り消すことができるのは理にかなっているが、廃棄はクリーンアップ、終了、リソースの解放などであり、一般的にはキャンセルすべきものではない。クリーンアップは、取り消される作業にとって依然として重要です。 実際の作業を取り消す原因となったのと同じ CancellationToken は、通常、DisposeAsyncに渡されるトークンと同じになり、作業の取り消しによって DisposeAsync が no-opになるため、DisposeAsync 価値がありません。 破棄を待機するブロックを回避したい場合は、結果として得られる ValueTask を待つのを避けるか、一定期間だけ待機することができます。
  • を返す : 非ジェネリック が存在し、から構築できるようになったので、 から を返すと、既存のオブジェクトを の最終的な非同期完了を表す promise として再利用でき、 が非同期で完了した場合に 割り当てが保存されます。
  • bool continueOnCapturedContext (ConfigureAwait) を使用した DisposeAsync の構成: このような概念が usingforeach、およびこれを使用する他の言語コンストラクトに公開される方法に関連する問題がある可能性がありますが、インターフェイスの観点からは、実際には 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つあり、小さいものと大きいものがあります。

  • マイナー: 列挙子が複数のコンシューマーをサポートできるようにします。 列挙子が複数の同時実行コンシューマーをサポートすることが重要なシナリオがある場合があります。 MoveNextAsyncCurrent が分離されている場合、実装で使用をアトミックにすることはできません。 これに対し、この方法では、列挙子を前方にプッシュして次の項目を取得することをサポートする 1 つのメソッド TryGetNext が提供されるため、必要に応じて列挙子がアトミック性を有効にすることができます。 ただし、このようなシナリオは、共有列挙可能な列挙子から各コンシューマーに独自の列挙子を与えることによっても有効にできる可能性があります。 さらに、すべての列挙子が同時使用をサポートするように強制することは望ましくありません。これは、それを必要としない大多数のケースに簡単ではないオーバーヘッドを追加するためです。つまり、インターフェイスのコンシューマーは一般的にこれに依存できません。
  • 主要: パフォーマンス. MoveNextAsync/Current アプローチでは、操作ごとに 2 つのインターフェイス呼び出しが必要ですが、WaitForNextAsync/TryGetNext の最適なケースは、ほとんどのイテレーションが同期的に完了し、TryGetNextでタイトな内部ループを有効にすることです。そのため、操作ごとにインターフェイス呼び出しは 1 つだけです。 これは、インターフェイス呼び出しが計算を支配する状況で測定可能な影響を与える可能性があります。

ただし、これらを手動で扱う場合の複雑さが著しく増し、それに伴いバグを発生させる可能性が高くなるなど、注意が必要な欠点があります。 また、パフォーマンス上の利点はマイクロベンチマークに現れますが、実際の使用の大部分で影響を受けるとは考えていません。 それが判明した場合は、2 つ目のインターフェイスセットを明るく導入できます。

検討された末に破棄されたオプション。

  • ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result);: out パラメーターを共変にすることはできません。 参照型の結果に対してランタイム書き込みバリアが発生する可能性が高いという小さな影響もあります(これは try パターン全体に共通する問題です)。

キャンセル

キャンセルをサポートするには、いくつかの方法があります。

  1. IAsyncEnumerable<T>/IAsyncEnumerator<T> はキャンセルに依存しません。CancellationToken はどこにも表示されません。 キャンセルは、反復子を呼び出すとき、CancellationToken を反復子メソッドに引数として渡し、他のパラメーターと同様に反復子の本体で使用するなど、適切な方法で CancellationToken を列挙可能な列挙子に論理的にベイクすることで実現されます。
  2. IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken): CancellationTokenGetAsyncEnumeratorに渡すと、後続の MoveNextAsync 操作は可能な限りそれを考慮します。
  3. IAsyncEnumerator<T>.MoveNextAsync(CancellationToken): 個々の CancellationToken 呼び出しに MoveNextAsync を渡します。
  4. 1 && 2: CancellationTokenを列挙可能型または列挙子に埋め込み、CancellationTokenGetAsyncEnumeratorに渡します。
  5. 1 && 3: CancellationToken を列挙可能/列挙子に埋め込み、CancellationTokenMoveNextAsync に渡します。

純粋に理論的な観点から見ると、(5) は最も堅牢です。(a) MoveNextAsyncCancellationToken を受け入れることで、キャンセルされた内容を最もきめ細かく制御でき、(b) CancellationToken は、任意の型に埋め込まれた反復子に引数として渡すことができる他の型にすぎません。

ただし、このアプローチには複数の問題があります。

  • GetAsyncEnumerator に渡された CancellationToken は、反復子の本体にどのように変換されますか? iterator に渡された CancellationToken にアクセスするために新しい GetEnumerator キーワードを導入し、それを使ってアクセスすることも可能ですが、a) それには多くの追加の処理が必要であり、b) これを非常に優先度の高い要素としています。c) 99% ケースで、イテレーターを呼び出し、かつそれに GetAsyncEnumerator を呼び出すための同一のコードに見えるため、その場合は CancellationToken をメソッドの引数として渡すことができます。
  • MoveNextAsync に渡された CancellationToken はどのようにしてメソッドの本体に入りますか? これはさらに悪いことに、iterator ローカル オブジェクトから公開されているかのように、その値は await 間で変更される可能性があります。つまり、トークンに登録されたコードは、待機する前に登録を解除してから、後で再登録する必要があります。また、反復子でコンパイラによって実装されているか、開発者によって手動で実装されているかに関係なく、すべての MoveNextAsync 呼び出しでこのような登録と登録解除を行う必要がある場合は、非常にコストがかかる可能性があります。
  • 開発者が foreach ループを取り消す方法 列挙可能/列挙子に CancellationToken を与えることによって行われる場合は、その後、a) foreach をサポートする必要があります。これにより、これを非常に優先度の高い要素となり、列挙子 (LINQ メソッドなど) に基づいて構築されたエコシステムについて考え始める必要があります。b) 提供されたトークンを格納する CancellationTokenWithCancellation 拡張メソッドを使用して、列挙型に IAsyncEnumerable<T> を埋め込む必要があります。返された構造体の GetAsyncEnumerator が呼び出されたときに、ラップされた列挙可能な GetAsyncEnumerator に渡します (そのトークンは無視されます)。 または、foreach の本体に含まれる CancellationToken だけを使用できます。
  • クエリの理解がサポートされる場合またはされる時に、CancellationTokenGetEnumeratorMoveNextAsync に指定して、各句に渡すにはどうすればよいでしょうか。 最も簡単な方法は、句がそれをキャプチャすることで、その結果、GetAsyncEnumerator/MoveNextAsync に渡されるトークンは無視されることになります。

このドキュメントの以前のバージョンでは (1) をお勧めしますが、(4) に切り替えました。

(1) の 2 つの主な問題:

  • 取り消し可能な列挙体のプロデューサーは、いくつかの定型句を実装する必要があり、非同期反復子に対するコンパイラのサポートを利用して IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken) メソッドを実装することしかできません。
  • 多くのプロデューサーは、代わりに非同期列挙可能なシグネチャに CancellationToken パラメーターを追加したくなる可能性があります。これにより、コンシューマーが IAsyncEnumerable 型を指定したときに必要なキャンセル トークンを渡すことができなくなります。

主に次の 2 つの消費シナリオがあります。

  1. コンシューマーが非同期イテレーター メソッドを呼び出す await foreach (var i in GetData(token)) ...
  2. コンシューマーが特定の IAsyncEnumerable インスタンスを扱う場所が await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ... です。

非同期ストリームのプロデューサーとコンシューマーの両方にとって便利な方法で両方のシナリオをサポートする妥当な妥協点は、非同期反復子メソッドで特別に注釈付けされたパラメーターを使用することです。 この目的には、[EnumeratorCancellation] 属性が使用されます。 この属性をパラメーターに配置すると、トークンが GetAsyncEnumerator メソッドに渡された場合は、パラメーターに最初に渡された値ではなく、そのトークンを使用する必要があることをコンパイラに指示します。

IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default)を検討してください。 このメソッドの実装者は、メソッド本体で単にパラメーターを使用できます。 コンシューマーは、上記のいずれかの消費パターンを使用できます。

  1. GetData(token)を使用する場合、トークンは非同期列挙可能に保存され、イテレーションで使用されます。
  2. 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とよく似ています)。 この決定は次のように進みます。

  • の型 Xdynamic または配列型の場合、エラーが生成され、それ以上の手順は実行されません。
  • それ以外の場合は、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 を使用することはできませんが、ConfigureAwaitTask/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 が有効かどうかを判断するために使用するためです。 ただし、必要ない場合でも、awaitasyncとしてマークされたメソッドでのみ使用できることは確立されており、一貫性を保つことが重要であると思われます。
  • IAsyncEnumerable<T>のカスタム ビルダーを有効にする: これは将来を見据えることができるものですが、機械は複雑であり、同期対応するユーザーにはサポートされていません。
  • シグネチャiterator キーワードがある: 非同期反復子はシグネチャで async iterator を使用し、yielditeratorを含む 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 foreachIObservable<T> を消費するには、前の項目がまだ処理中のときに別の項目がプッシュされた場合に備えてデータをバッファリングする必要がありますが、そのようなプッシュプルアダプターを簡単に実装することで、IAsyncEnumerator<T>を使用して IObservable<T> を取得することができます。 Rx/Ixは既にそのような実装のプロトタイプを提供しており、https://github.com/dotnet/corefx/tree/master/src/System.Threading.Channels のようなライブラリはさまざまな種類のバッファリングデータ構造を提供しています。 この段階では、言語を使用する必要はありません。