web-dev-qa-db-ja.com

非同期はlinq selectで待ちます

既存のプログラムを修正する必要があり、それには以下のコードが含まれています。

var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

しかし、これは私にとって非常に奇妙に思えます。まず最初にselectでasyncawaitを使用します。によると この答え Stephen Clearyによると私はそれらを落とすことができるはずです。

次に、結果を選択する2番目のSelect。これは、タスクがまったく非同期ではなく同期的に実行されることを意味するのではありませんか(それほど努力が必要ではありません)。

上記のコードを Stephen Clearyによる別の回答 に従って次のように書く必要があります。

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

これはまったく同じですか。

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

私はこのプロジェクトに取り組んでいる間、私は最初のコードサンプルを変更したいのですが、非同期コードを変更することに熱心ではありません。たぶん私は何も心配しておらず、3つのコードサンプルすべてがまったく同じことをしているのでしょうか。

ProcessEventsAsyncは次のようになります。

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}
121
Alexander Derck
var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

しかし、これは私にとって非常に奇妙に思えます、まず第一に非同期の使用と選択で待ちます。 Stephen Clearyによるこの回答によると、私はそれらを落とすことができるはずです。

Selectの呼び出しは有効です。これら2行は基本的に同じです。

events.Select(async ev => await ProcessEventAsync(ev))
events.Select(ev => ProcessEventAsync(ev))

(同期例外がProcessEventAsyncからどのようにスローされるかについては多少の違いがありますが、このコードの文脈ではまったく問題になりません。)

それから2番目のSelectは結果を選択します。これは、タスクがまったく非同期ではなく同期的に実行されることを意味するのではありませんか(それほど努力しなくても)、タスクは非同期的に実行され、完了すると残りのクエリが実行されます。

クエリがブロックされていることを意味します。だからそれは本当に非同期ではありません。

それを分解する:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))

最初に各イベントに対して非同期操作を開始します。それからこの行:

                   .Select(t => t.Result)

これらの操作が一度に1つずつ完了するのを待ちます(最初に最初のイベントの操作を待機し、次に次の操作、次に次の操作など)。

これは私が気にしない部分です。なぜなら、それはブロックし、またどんな例外もAggregateExceptionでラップするからです。

これはまったく同じですか。

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

はい、これら2つの例は同等です。どちらもすべての非同期操作を開始し(events.Select(...))、次にすべての操作が任意の順序で完了するのを非同期に待機し(await Task.WhenAll(...))、残りの作業を続行します(Where...)。

これらの例はどちらも元のコードとは異なります。元のコードはブロックされており、例外をAggregateExceptionでラップします。

129
Stephen Cleary

既存のコードは機能していますが、スレッドをブロックしています。

.Select(async ev => await ProcessEventAsync(ev))

イベントごとに新しいタスクを作成しますが、

.Select(t => t.Result)

新しいタスクが終了するのを待つスレッドをブロックします。

一方、あなたのコードは同じ結果を生成しますが非同期を保ちます。

あなたの最初のコードについて一つだけコメントしてください。この行

var tasks = await Task.WhenAll(events...

単一のタスクを生成するので、変数は単数形で命名されるべきです。

最後にあなたの最後のコードは同じになりますが、より簡潔です

参考: Task.Wait / Task.WhenAll

18
tede24

Linqで利用可能な現在のメソッドでは、それはかなり醜く見えます:

var tasks = items.Select(
    async item => new
    {
        Item = item,
        IsValid = await IsValid(item)
    });
var tuples = await Task.WhenAll(tasks);
var validItems = tuples
    .Where(p => p.IsValid)
    .Select(p => p.Item)
    .ToList();

うまくいけば、.NETの次のバージョンがタスクのコレクションとコレクションのタスクを処理するためのより洗練されたツールを思い付くでしょう。

6

拡張方法としてこれを好む:

public static async Task<IEnumerable<T>> WhenAll<T>(this IEnumerable<Task<T>> tasks)
{
    return await Task.WhenAll(tasks);
}

メソッドチェーンで使えるように:

var inputs = await events
  .Select(async ev => await ProcessEventAsync(ev))
  .WhenAll()
3
Daryl

私はこのコードを使いました:

public static async Task<IEnumerable<TResult>> SelectAsync<TSource,TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> method)
{
      return await Task.WhenAll(source.Select(async s => await method(s)));
}

このような:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));
0