結果が異なる複数のタスクを待機しています


237

私には3つのタスクがあります。

private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}

私のコードを続行する前に、これらすべてを実行する必要があり、それぞれの結果も必要です。結果に共通点はありません

3つのタスクが呼び出されて完了するのを待って結果を取得するにはどうすればよいですか?


25
注文要件はありますか?つまり、猫に餌をやるまで家を売りたくないですか?
Eric Lippert 2013年

回答:


411

を使用した後、次を使用しWhenAllて結果を個別に取り出すことができますawait

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

使用することもできますTask.Result(この時点ですべてが正常に完了していることがわかります)。ただし、他のシナリオでは問題が発生する可能性があるawaitため、使用することをお勧めしResultます。


83
WhenAllこれを完全に削除できます。awaitsは、タスクがすべて完了するまで、後の3つの割り当てを超えないようにします。
2013年

134
Task.WhenAll()タスクを並列モードで実行できます。@Servyが削除を提案した理由を理解できません。WhenAllこれらがなければ、1つずつ実行されます
セルゲイG.

87
@Sergey:タスクはすぐに実行を開始します。たとえば、catTaskから返されるまでにはすでに実行されていFeedCatます。したがって、どちらのアプローチでも機能します。唯一の問題はawait、一度に1つずつにするか、すべてまとめて欲しいかです。エラー処理は少し異なります。を使用するとTask.WhenAllawaitいずれかが早期に失敗しても、すべてが処理されます。
Stephen Cleary 2015年

23
@Sergey Calling WhenAllは、操作がいつ実行されるか、またはどのように実行されるかに影響を与えません。これは、唯一の任意の持っている可能性の結果が観察されている方法をもたらすことを。この特定の場合の唯一の違いは、最初の2つのメソッドのいずれかでエラーが発生すると、このコールスタックで例外がスローされ、Stephenのメソッドよりも先にメソッドでスローされます(ただし、同じエラーが常にスローされますが、 )。
サービー2015年

37
@Sergey:重要なのは、非同期メソッドが常に「ホット」(すでに開始されている)タスクを返すことです。
Stephen Cleary 2015年

99

ただ、awaitそれらすべてを開始した後、別々の3つのタスク、。

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

8
@Bargittaいいえ、それは誤りです。彼らは並行して仕事をします。それを実行して、自分の目で確かめてください。
16年

5
人々は何年後に同じ質問をしておく...私は仕事「ということを再度強調することが重要であると感じ作成上の開始のボディに」答えはおそらく彼らがコメントを読んで気にしないでください:

9
@StephenYork Task.WhenAll変更を追加すると、プログラムの動作について文字どおり何も観察可能な方法で変更されません。これは純粋に冗長なメソッド呼び出しです。必要に応じて、美的選択として追加することもできますが、コードの動作は変わりません。コードの実行時間は、そのメソッド呼び出しの有無にかかわらず同じです(技術的にはを呼び出すためのオーバーヘッドは非常にわずかですがWhenAll、これは無視できるはずです)。そのバージョンは、このバージョンよりも実行時間がわずかに長くなります。
巡礼

4
@StephenYorkこの例では、2つの理由で操作を順次実行します。非同期メソッドは実際には非同期ではなく、同期です。常に完了済みのタスクを常に返す同期メソッドがあるため、それらを同時に実行することはできません。次に、あなたが実際にすべての3つの非同期メソッドを起動すると、この答えに示されている何をしていないその後、順番に3つのタスクを待っています。このコードとは異なり、この例では、前のメソッドが終了するまで各メソッドを呼び出さないため、前のメソッドが終了するまでメソッドが開始されないことが明示的に禁止されています。
軍人

4
@MarcvanNieuwenhuijzenここのコメントや他の回答で議論されているように、それは明らかに真実ではありません。追加WhenAllは純粋に美的変更です。観察可能な唯一の動作の違いは、前のタスクで障害が発生した場合に、後のタスクが完了するのを待つかどうかです。通常、その必要はありません。ステートメントが真ではない理由についての多数の説明を信じられない場合は、自分でコードを実行して、それが真ではないことを確認できます。
サービー

37

C#7を使用している場合は、次のような便利なラッパーメソッドを使用できます...

public static class TaskEx
{
    public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
    {
        return (await task1, await task2);
    }
}

...戻り値の種類が異なる複数のタスクを待機したいときに、このような便利な構文を有効にします。もちろん、待機するタスクの数が異なる場合は、複数のオーバーロードを作成する必要があります。

var (someInt, someString) = await TaskEx.WhenAll(GetIntAsync(), GetStringAsync());

ただし、この例を現実のものにしようと考えている場合は、ValueTaskおよび既に完了したタスクに関するいくつかの最適化について、Marc Gravellの回答を参照してください。


タプルは、ここに含まれる唯一のC#7機能です。それらは間違いなく最終リリースにあります。
Joel Mueller

私はタプルとc#7について知っています。つまり、タプルを返すWhenAllメソッドを見つけることができません。どんな名前空間/パッケージ?
Yury Scherbakov 2017

@YuryShcherbakov Task.WhenAll()はタプルを返しません。1つはResult、によって返されたタスクのTask.WhenAll()完了後に、提供されたタスクのプロパティから構築されています。
Chris Charabaruk

2
.Result他の人があなたの例をコピーすることによって悪い習慣を永続させるのを避けるために、スティーブンの推論に従って呼び出しを置き換えることをお勧めします。
julealgon

このメソッドがフレームワークのこの部分ではないのはなぜですか?とても便利そうです。彼らは時間切れになり、単一の戻り値の型で停止する必要がありましたか?
Ian Grainger、

14

3つのタスクを考える- FeedCat()SellHouse()およびBuyCar()、2興味深い例があります。どちらか彼ら(何らかの理由で、おそらくキャッシュまたはエラー)すべての完全な同期、またはそうではありません。

質問から、私たちは持っているとしましょう:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    // what here?
}

今、簡単なアプローチは次のようになります:

Task.WhenAll(x, y, z);

しかし...これは結果の処理には不便です。通常は、次のようにawaitします。

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    await Task.WhenAll(x, y, z);
    // presumably we want to do something with the results...
    return DoWhatever(x.Result, y.Result, z.Result);
}

しかし、これは多くのオーバーヘッドを行い、さまざまな配列(配列を含むparams Task[])とリスト(内部)を割り当てます。動作しますが、IMOは素晴らしいものではありません。多くの点で、操作を順番に使用する方が簡単です。asyncawait

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    // do something with the results...
    return DoWhatever(await x, await y, await z);
}

上記のコメントの一部とは異なり、await代わりにTask.WhenAllを使用しても、タスクの実行方法に(同時、順次など)違いはありません。最高レベルでTask.WhenAll async/のコンパイラサポートが以前awaitからあり、それらが存在しない場合に役立ちまし。3つの目立たないタスクではなく、タスクの任意の配列がある場合にも役立ちます。

しかし、私たちはまだasync/ awaitが継続のために多くのコンパイラノイズを生成するという問題を抱えています。タスク実際に同期的に完了する可能性がある場合は、非同期フォールバックを使用して同期パスを構築することにより、これを最適化できます。

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    if(x.Status == TaskStatus.RanToCompletion &&
       y.Status == TaskStatus.RanToCompletion &&
       z.Status == TaskStatus.RanToCompletion)
        return Task.FromResult(
          DoWhatever(a.Result, b.Result, c.Result));
       // we can safely access .Result, as they are known
       // to be ran-to-completion

    return Awaited(x, y, z);
}

async Task Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
    return DoWhatever(await x, await y, await z);
}

この「同期パスと非同期フォールバック」アプローチは、特に同期完了が比較的頻繁に行われる高性能コードでますます一般的になっています。完了が常に完全に非同期である場合は、まったく役に立たないことに注意してください。

ここで適用される追加事項:

  1. 最近のC#では、一般的なパターンは、asyncフォールバックメソッドがローカル関数として一般的に実装されているものです。

    Task<string> DoTheThings() {
        async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        Task<Cat> x = FeedCat();
        Task<House> y = SellHouse();
        Task<Tesla> z = BuyCar();
    
        if(x.Status == TaskStatus.RanToCompletion &&
           y.Status == TaskStatus.RanToCompletion &&
           z.Status == TaskStatus.RanToCompletion)
            return Task.FromResult(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  2. 好みValueTask<T>Task<T>多くの異なる戻り値で、これまでに完全に同期物事の良いチャンスがあれば:

    ValueTask<string> DoTheThings() {
        async ValueTask<string> Awaited(ValueTask<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        ValueTask<Cat> x = FeedCat();
        ValueTask<House> y = SellHouse();
        ValueTask<Tesla> z = BuyCar();
    
        if(x.IsCompletedSuccessfully &&
           y.IsCompletedSuccessfully &&
           z.IsCompletedSuccessfully)
            return new ValueTask<string>(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  3. 可能であれば、以下を優先IsCompletedSuccessfullyしてくださいStatus == TaskStatus.RanToCompletion。これは.NET Core for Task、およびどこでもValueTask<T>


「ここでのさまざまな答えに反して、Task.WhenAllの代わりにawaitを使用しても、タスクの実行方法に(同時、順次など)違いはありません。」そのような答えは見当たりません。彼らが言ったのと同じくらい多くのことを言っている彼らにすでに私はコメントしたでしょう。それを言っている多くの答えにたくさんのコメントがありますが、答えはありません。あなたはどちらを指しているのですか?また、あなたの答えはタスクの結果を処理しないことに注意してください(または結果がすべて異なるタイプであるという事実に対処します)。Task結果を使用せずにすべて完了したときにaを返すメソッドでそれらを構成しました。
サービー

@Servyあなたは正しい、それはコメントだった。結果を使用して表示する微調整を追加します
Marc Gravell

@Servyの微調整を追加
Marc Gravell

また、同期タスクを早期に処理する場合は、正常に完了したタスクだけでなく、同期的にキャンセルまたは障害が発生したタスクを処理することもできます。それがプログラムに必要な最適化であると決定した場合(これはまれですが、発生する可能性があります)、最後まで進んでもよいでしょう。
サービー2018

複雑なトピックである@Servy-2つのシナリオから異なる例外セマンティクスを取得します-例外のトリガーを待機する動作は、.Resultにアクセスして例外をトリガーする動作とは異なります。IMOその時点で我々はすべきawait例外はまれではあるが意味があることを前提に、「より良い」例外セマンティクスを取得するには
マルクGravell

12

あなたはそれらをタスクに保存して、それらすべてを待つことができます:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

Cat cat = await catTask;
House house = await houseTask;
Car car = await carTask;

メソッドがすでに実行されているのでvar catTask = FeedCat()、関数FeedCat()を実行して結果を格納せずcatTask、そのawait Task.WhenAll()部分を役に立たないものにしますか?
Kraang Prime

1
@sanuel彼らがタスク<t>を返す場合、いいえ...彼らは非同期オープンを開始しますが、それを待たない
Reed Copsey

これは正確ではないと思います。@ StephenClearyの回答の下の説明を参照してください... Servyの回答も参照してください。
Rosdi Kasim 2017

1
.ConfigrtueAwait(false)を追加する必要がある場合。それをTask.WhenAllだけに追加するのか、それとも後続の各待機者に追加するのですか?
AstroSharp 2017

@AstroSharpは一般に、すべてに追加することをお勧めします(最初のものが完了した場合、効果的に無視されます)が、この場合、おそらく最初の処理だけを行っても問題ありません。後で起こるもの。
リードコプシー2017

6

すべてのエラーをログに記録しようとしている場合は、コード内のTask.WhenAll行を確実に保持してください。多くのコメントでは、それを削除して個々のタスクを待つことができます。Task.WhenAllはエラー処理にとって本当に重要です。この行がないと、監視されない例外のためにコードを開いたままにする可能性があります。

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

FeedCatが次のコードで例外をスローするとします。

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

その場合、houseTaskやcarTaskを待つことはありません。ここには3つのシナリオが考えられます。

  1. FeedCatが失敗した場合、SellHouseは既に正常に完了しています。この場合は大丈夫です。

  2. SellHouseは完全ではなく、ある時点で例外が発生して失敗します。例外は発生せず、ファイナライザスレッドで再スローされます。

  3. SellHouseは完全ではなく、内部に待機が含まれています。コードがASP.NETで実行されている場合、SellHouseはその内部でいくつかの待機が完了するとすぐに失敗します。これは、FeedCatが失敗するとすぐに基本的に起動して忘れた呼び出しを行い、同期コンテキストが失われたために発生します。

ケース(3)で発生するエラーは次のとおりです。

System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()<---

ケース(2)の場合、同様のエラーが発生しますが、元の例外スタックトレースが表示されます。

.NET 4.0以降では、TaskScheduler.UnobservedTaskExceptionを使用して、監視されていない例外をキャッチできます。.NET 4.5以降の場合、監視されない例外はデフォルトで飲み込まれます。.NET4.0の場合、監視されない例外はプロセスをクラッシュさせます。

詳細はこちら:.NET 4.5でのタスク例外処理


2

スレッドを待機させるかどうかに応じて、Task.WhenAll前述のように、またはを使用できますTask.WaitAll。両方の説明については、リンクを参照してください。

WaitAllとWhenAll


2

Task.WhenAll結果を使用してから待ちます。

var tCat = FeedCat();
var tHouse = SellHouse();
var tCar = BuyCar();
await Task.WhenAll(tCat, tHouse, tCar);
Cat cat = await tCat;
House house = await tHouse;
Tesla car = await tCar; 
//as they have all definitely finished, you could also use Task.Value.

mm ... Task.Valueではなく(おそらく2013年に存在していたのでしょうか?)ではなく、tCat.Result、tHouse.ResultまたはtCar.Result
Stephen York

1

前方警告

このスレッドや他の同様のスレッドにアクセスし、async + await + taskツールセットを使用てEntityFrameworkを並列化する方法を探している人たちに、さっと手に取ってください。:ここに示すパターンは適切ですが、EFの特別なスノーフレークに関しては、関与するすべての* Async()呼び出し内で個別の(新しい)db-context-instanceを使用しない限り、並列実行を実現します。

同じ種類のef-db-contextインスタンスで複数のクエリを並行して実行することを禁止するef-db-contextsの固有の設計上の制限により、このようなことが必要になります。


これは、すでに与えられた回答を利用して、1つ以上のタスクが例外になる場合でも、すべての値を確実に収集する方法です。

  public async Task<string> Foobar() {
    async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
        return DoSomething(await a, await b, await c);
    }

    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        if (carTask.Status == TaskStatus.RanToCompletion //triple
            && catTask.Status == TaskStatus.RanToCompletion //cache
            && houseTask.Status == TaskStatus.RanToCompletion) { //hits
            return Task.FromResult(DoSomething(catTask.Result, carTask.Result, houseTask.Result)); //fast-track
        }

        cat = await catTask;
        car = await carTask;
        house = await houseTask;
        //or Task.AwaitAll(carTask, catTask, houseTask);
        //or await Task.WhenAll(carTask, catTask, houseTask);
        //it depends on how you like exception handling better

        return Awaited(catTask, carTask, houseTask);
   }
 }

ほぼ同じパフォーマンス特性を持つ別の実装は次のとおりです。

 public async Task<string> Foobar() {
    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        cat = catTask.Status == TaskStatus.RanToCompletion ? catTask.Result : (await catTask);
        car = carTask.Status == TaskStatus.RanToCompletion ? carTask.Result : (await carTask);
        house = houseTask.Status == TaskStatus.RanToCompletion ? houseTask.Result : (await houseTask);

        return DoSomething(cat, car, house);
     }
 }

-1
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

Catにアクセスする場合は、次のようにします。

var ct = (Cat)dn[0];

これは非常に簡単で、非常に便利です。複雑なソリューションを探す必要はありません。


1
これにはただ1つの問題がありdynamicます。それは悪魔です。これはトリッキーなCOM相互運用機能などのためのものであり、絶対に必要ではない状況では使用しないでください。特にパフォーマンスを気にする場合。またはタイプセーフ。またはリファクタリング。またはデバッグ。
Joel Mueller、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.