20

次のような既存のコードがあります。

IEnumerable<SomeClass> GetStuff()
{
    using (SqlConnection conn = new SqlConnection(connectionString))
    using (SqlCommand cmd = new SqlCommand(sql, conn)
    {
        conn.Open();
        SqlDataReader reader = cmd.ExecuteReader();
        while (reader.Read())
        {
            SomeClass someClass = f(reader); // create instance based on returned row
            yield return someClass;
        }
    } 
}

を使用することでメリットが得られるようですreader.ReadAsync()。ただし、1行を変更するだけの場合:

        while (await reader.ReadAsync())

コンパイラは、awaitでマークされたメソッドでのみ使用できることを通知しasync、メソッドのシグネチャを次のように変更することを提案します。

async Task<IEnumerable<SomeClass>> GetStuff()

ただし、これを行うと、次のGetStuff()理由で使用できなくなります。

はイテレータインターフェイスタイプではないため、の本体をGetStuff()イテレータブロックにすることはできません。Task<IEnumerable<SomeClass>>

非同期プログラミングモデルの重要な概念が欠けていると確信しています。

質問:

  • ReadAsync()イテレータで使用できますか?どのように?
  • この種の状況で非同期パラダイムがどのように機能するかを理解するために、非同期パラダイムをどのように異なる方法で考えることができますか?
4

4 に答える 4

21

問題は、あなたが求めていることは実際にはあまり意味がないということです。IEnumerable<T>は同期インターフェースであり、戻るTask<IEnumerable<T>>ことはあまり役に立ちません。何があっても、一部のスレッドは各アイテムの待機をブロックする必要があるためです。

実際に返したいのは、C#8.0 / .NetCore3.0に追加される予定のTPLDataflowまたはのdataflowブロックのIEnumerable<T>ような非同期の代替手段です。(そして、その間に、それを含むいくつかのライブラリがあります。)IObservable<T>IAsyncEnumerable<T>

TPL Dataflowを使用すると、これを行う1つの方法は次のようになります。

ISourceBlock<SomeClass> GetStuff() {
    var block = new BufferBlock<SomeClass>();

    Task.Run(async () =>
    {
        using (SqlConnection conn = new SqlConnection(connectionString))
        using (SqlCommand cmd = new SqlCommand(sql, conn))
        {
            await conn.OpenAsync();
            SqlDataReader reader = await cmd.ExecuteReaderAsync();
            while (await reader.ReadAsync())
            {
                SomeClass someClass;
                // Create an instance of SomeClass based on row returned.
                block.Post(someClass);
            }
            block.Complete();
        } 
    });

    return block;
}

上記のコードにエラー処理を追加することをお勧めしますが、それ以外の場合は機能するはずであり、完全に非同期になります。

コードの残りの部分は、おそらくを使用して、返されたブロックからのアイテムも非同期的に消費しますActionBlock

于 2012-10-30T22:45:27.337 に答える
20

いいえ、現在、イテレータブロックで非同期を使用することはできません。svickが言うように、あなたはそれをするような何かが必要になるでしょうIAsyncEnumerable

戻り値がある場合はTask<IEnumerable<SomeClass>>、関数が単一のTaskオブジェクトを返すことを意味します。このオブジェクトが完了すると、完全に形成されたIEnumerableが提供されます(この列挙型にはタスクの非同期の余地はありません)。タスクオブジェクトが完了すると、呼び出し元は、列挙可能オブジェクトに返されたすべてのアイテムを同期的に反復できるようになります。

これがを返すソリューションですTask<IEnumerable<SomeClass>>。次のようなことを行うことで、非同期のメリットの大部分を得ることができます。

async Task<IEnumerable<SomeClass>> GetStuff()
{
    using (SqlConnection conn = new SqlConnection(""))
    {
        using (SqlCommand cmd = new SqlCommand("", conn))
        {
            await conn.OpenAsync();
            SqlDataReader reader = await cmd.ExecuteReaderAsync();
            return ReadItems(reader).ToArray();
        }
    }
}

IEnumerable<SomeClass> ReadItems(SqlDataReader reader)
{
    while (reader.Read())
    {
        // Create an instance of SomeClass based on row returned.
        SomeClass someClass = null;
        yield return someClass;
    }
}

...および使用例:

async void Caller()
{
    // Calls get-stuff, which returns immediately with a Task
    Task<IEnumerable<SomeClass>> itemsAsync = GetStuff();
    // Wait for the task to complete so we can get the items
    IEnumerable<SomeClass> items = await itemsAsync;
    // Iterate synchronously through the items which are all already present
    foreach (SomeClass item in items)
    {
        Console.WriteLine(item);
    }
}

ここでは、イテレータ部分と非同期部分が別々の関数にあり、非同期構文とyield構文の両方を使用できます。このGetStuff関数は非同期でデータを取得し、ReadItems次に同期的にデータを列挙可能に読み取ります。

ToArray()呼び出しに注意してください。列挙子関数は遅延して実行されるため、このようなものが必要です。そうしないと、すべてのデータが読み取られる前に、非同期関数が接続とコマンドを破棄する可能性があります。これは、usingブロックがTask実行期間をカバーしているためですがafter、タスクが完了したことを繰り返すことになります。

このソリューションはを使用しませReadAsyncが、とを使用します。これにより、おそらくほとんどのメリットが得られます。私の経験では、非同期であることが最も時間がかかり、最もメリットがあるのはExecuteReaderです。最初の行を読んだ時点で、には他のすべての行がすでにあり、同期して戻ってきます。これがあなたにも当てはまる場合は、のようなプッシュベースのシステムに移行しても大きなメリットは得られません(呼び出し元の関数に大幅な変更が必要になります)。OpenAsyncExecuteReaderAsyncSqlDataReaderReadAsyncIObservable<T>

説明のために、同じ問題に対する別のアプローチを検討してください。

IEnumerable<Task<SomeClass>> GetStuff()
{
    using (SqlConnection conn = new SqlConnection(""))
    {
        using (SqlCommand cmd = new SqlCommand("", conn))
        {
            conn.Open();
            SqlDataReader reader = cmd.ExecuteReader();
            while (true)
                yield return ReadItem(reader);
        }
    }
}

async Task<SomeClass> ReadItem(SqlDataReader reader)
{
    if (await reader.ReadAsync())
    {
        // Create an instance of SomeClass based on row returned.
        SomeClass someClass = null;
        return someClass;
    }
    else
        return null; // Mark end of sequence
}

...および使用例:

async void Caller()
{
    // Synchronously get a list of Tasks
    IEnumerable<Task<SomeClass>> items = GetStuff();
    // Iterate through the Tasks
    foreach (Task<SomeClass> itemAsync in items)
    {
        // Wait for the task to complete. We need to wait for 
        // it to complete before we can know if it's the end of
        // the sequence
        SomeClass item = await itemAsync;
        // End of sequence?
        if (item == null) 
            break;
        Console.WriteLine(item);
    }
}

この場合、GetStuffenumerableですぐに戻ります。ここで、enumerableの各アイテムは、SomeClass完了時にオブジェクトを提示するタスクです。このアプローチにはいくつかの欠点があります。まず、列挙型は同期的に返されるため、返される時点では、結果に含まれる行数が実際にはわかりません。そのため、無限シーケンスにしました。これは完全に合法ですが、いくつかの副作用があります。私は使用する必要がありましたnullタスクの無限のシーケンスで有用なデータの終わりを知らせるため。第二に、あなたはそれをどのように繰り返すかについて注意しなければなりません。前方に反復する必要があり、次の行に反復する前に各行を待つ必要があります。また、すべてのタスクが完了した後にのみイテレータを破棄して、GCが使用を終了する前に接続を収集しないようにする必要があります。これらの理由から、これは安全な解決策ではありません。2番目の質問に答えるために、説明のためにこれを含めていることを強調する必要があります。

于 2012-10-31T00:17:34.190 に答える
2

SqlCommand私の経験では、のコンテキスト内で非同期イテレータ(または可能性がある)と厳密に言えば、同期バージョンのコードがasync対応するバージョンよりも大幅に優れていることに気付きました。速度とメモリ消費の両方で。

おそらく、テストの範囲は私のマシンとローカルSQL Serverインスタンスに限定されていたので、この観察結果を一目で理解してください。

誤解しないでください。.NET環境内のasync/awaitパラダイムは、適切な状況を考えると、驚くほどシンプルで強力で便利です。しかし、多くの労力を費やした後、データベースアクセスがその適切なユースケースであるとは確信していません。もちろん、複数のコマンドを同時に実行する必要がある場合を除いて、その場合は、TPLを使用してコマンドを同時に実行できます。

私が好むアプローチは、次のことを考慮することです。

  • SQLの単位を小さく、シンプルで、構成可能に保ちます(つまり、SQLの実行を「安価」にします)。
  • アプリレベルにアップストリームでプッシュできるSQLServerでの作業は避けてください。これの完璧な例はソートです。
  • 最も重要なことは、SQLコードを大規模にテストし、StatisticsIOの出力/実行プランを確認することです。10kレコードで高速に実行されるクエリは、100万レコードの場合、まったく異なる動作をする可能性があります(おそらくそうなるでしょう)。

特定のレポートシナリオでは、上記の要件の一部が不可能であると主張することができます。ただし、レポートサービスのコンテキストでは、非同期性(それは一言でさえありますか?)が本当に必要ですか?

このトピックについて、マイクロソフトのエバンジェリストであるリックアンダーソンによる素晴らしい記事があります。古い(2009年から)が、それでも非常に関連性があることに注意してください。

于 2017-08-13T14:20:38.117 に答える
1

C#8の時点で、これはIAsyncEnumerableを使用して実現できます。

変更されたコード:

async IAsyncEnumerable<SomeClass> GetStuff()
{
    using (SqlConnection conn = new SqlConnection(connectionString))
    using (SqlCommand cmd = new SqlCommand(sql, conn)
    {
        conn.Open();
        SqlDataReader reader = cmd.ExecuteReader();
        while (reader.Read())
        {
            SomeClass someClass = f(reader); // create instance based on returned row
            yield return someClass;
        }
    } 
}

このように消費します:

await foreach (var stuff in GetStuff())
    ...
于 2021-12-14T15:27:36.163 に答える