LINQ SELECTで非同期待機


180

既存のプログラムを変更する必要があり、次のコードが含まれています。

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

しかし、これは最初のすべての使用の、私にとって非常に奇妙なようだasyncし、await選択します。スティーブン・クリアリーのこの回答によると私はそれらを落とすことができるはずです。

次に、Select結果を選択する2番目。これは、タスクがまったく非同期ではなく、同期的に実行される(何もしないために多大な労力を要する)ことを意味しませんか?それとも、タスクは非同期的に実行され、完了したら残りのクエリが実行されますか?

私はに応じて次のように上記のコードを記述する必要がありステファン・クリアリーによって別の答え

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) {...}

ProceesEventAsyncの戻り値の型は何ですか?
tede24

@ tede24それだTask<InputResult>InputResultカスタムクラスであること。
Alexander Derck 16年

私の意見では、あなたのバージョンは読みやすくなっています。ただし、のSelect前のタスクの結果を忘れてしまいました Where
最大

そして、InputResultにはResultプロパティがありますか?
tede24

@ tede24結果はクラスではなくタスクのプロパティです。そして、@ Max awaitはResult、タスクのプロパティにアクセスせずに結果を確実に取得する必要があります
Alexander Derck

回答:


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

しかし、これは私には非常に奇妙に思えます。まず、非同期を使用して、selectで待機します。スティーブン・クリアリーのこの回答によると、私はそれらを落とせるはずです。

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

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

(から同期例外がスローされる方法に関して若干の違いがありProcessEventAsyncますが、このコードのコンテキストではまったく問題ではありません。)

次に、結果を選択する2番目の選択。これは、タスクがまったく非同期ではなく、同期的に実行される(何もしないために多大な労力を要する)ことを意味しませんか?それとも、タスクは非同期的に実行され、完了したら残りのクエリが実行されますか?

これは、クエリがブロックされていることを意味します。したがって、実際には非同期ではありません。

それを分解する:

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ます。


それを片付けるための乾杯!では、例外をAggregateException1つにまとめる代わりに、2番目のコードで複数の個別の例外を取得しますか?
Alexander Derck 16年

1
@AlexanderDerck:いいえ、古いコードと新しいコードの両方で、最初の例外のみが発生しました。しかし、Resultそれはに包まれAggregateExceptionます。
Stephen Cleary 2016年

このコードを使用すると、ASP.NET MVCコントローラーでデッドロックが発生します。Task.Run(…)を使用して解決しました。気分が悪いです。ただし、非同期のxUnitテストを実行すると、正常に終了しました。どうしたの?
SuperJMN

2
@SuperJMN:交換するstuff.Select(x => x.Result);await Task.WhenAll(stuff)
ステファン・クリアリー

1
@DanielS:基本的には同じです。ステートマシン、キャプチャコンテキスト、同期例外の動作など、いくつかの違いがあります。blog.stephencleary.com/2016/12/eliding-async-await.htmlの
Stephen Cleary

25

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

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

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

.Select(t => t.Result)

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

一方、コードは同じ結果を生成しますが、非同期のままです。

最初のコードに関するコメントは1つだけです。この線

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

単一のタスクを生成するため、変数は単数形で名前を付ける必要があります。

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

参考:Task.Wait / Task.WhenAll


では、最初のコードブロックは実際には同期的に実行されますか?
Alexander Derck 16年

1
はい。結果にアクセスすると、スレッドをブロックする待機が発生するためです。一方、新しいタスクが生成されると、待機することができます。
tede24

1
この質問に戻り、tasks変数の名前についてのあなたの意見を見ると、あなたは完全に正しいです。恐ろしい選択です。すぐに待たされるので、それらはタスクでさえありません。質問はそのままにしておきます
Alexander Derck

13

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の次のバージョンでは、タスクのコレクションとコレクションのタスクを処理するためのより洗練されたツールが登場するでしょう。


12

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

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));

5
これは、既存の機能をもっとあいまいな方法でラップしただけです
Alexander Derck '22

代替はvar result = await Task.WhenAll(sourceEnumerable.Select(async s => await someFunction(s、other params))でも機能しますが、LINQyではありません
Siderite Zackwehdex

コードの2番目のビットに記載さFunc<TSource, Task<TResult>> methodれてother paramsいる内容を含めるべきではありませんか?
マトラモス

2
追加のパラメーターは外部であり、実行する関数によって異なりますが、拡張メソッドのコンテキストには関係ありません。
Siderite Zackwehdex 2018

4
それは素敵な拡張メソッドです。「もっとあいまい」と見なされた理由がわからない-これは意味的に同期Select()に似ているため、エレガントなドロップインです。
nullPainter

10

私はこれを拡張メソッドとして好みます:

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()

1
Wait実際に待機していないときにメソッドを呼び出すことはできません。すべてのタスクが完了したときに完了するタスクを作成しています。エミュレートWhenAllするTaskメソッドと同様に、それを呼び出します。メソッドがであっても意味がありませんasync。電話するだけWhenAllで完了です。
サービー

私の意見では、元のメソッドを呼び出すだけの少し役に立たないラッパー
Alexander Derck

@Servyフェアポイントですが、名前のオプションは特に好きではありません。WhenAllを使用すると、イベントのように聞こえますが、これは完全ではありません。
ダリル

3
@AlexanderDerckの利点は、メソッドチェーンで使用できることです。
ダリル

1
@Daryl WhenAllは評価されたリストを返すため(遅延評価されない)、Task<T[]>戻り値の型を使用してそれを示すように引数を作成できます。待つと、これは引き続きLinqを使用できますが、怠惰ではないことも伝えます。
JAD、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.