web-dev-qa-db-ja.com

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

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

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

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

3つのタスクが完了して結果を取得するのをどのように呼び出して待機しますか?

177

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を使用することをお勧めします。

303
Stephen Cleary

すべてを開始した後、3つのタスクを別々にawaitします。

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

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

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

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

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

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

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

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;
10
Reed Copsey

FeedCat()SellHouse()、およびBuyCar()の3つのタスクを考えると、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ではありません。多くの点で、asyncオペレーションを使用して、それぞれawaitを順番に使用することはsimplerです

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

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

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

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 fallbackメソッドがローカル関数として一般的に実装されています:

    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. 可能であれば、Status == TaskStatus.RanToCompletionよりIsCompletedSuccessfullyを優先してください。これはTaskの.NET Coreに存在し、ValueTask<T>のすべての場所に存在します

8
Marc Gravell

すべてのエラーをログに記録しようとする場合、コードに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. FeedHoatが失敗すると、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以降では、.NET 4.0の未確認の例外はデフォルトで飲み込まれ、未確認の例外はプロセスをクラッシュさせます。

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

3
samfromlv

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

WaitAll vs WhenAll

2
christiandev

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.
2
It'sNotALie.
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

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

var ct = (Cat)dn[0];

これは非常に簡単で使いやすく、複雑なソリューションを追求する必要はありません。

1
taurius

前方警告

async + await + task tool-setを使用して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);
     }
 }
0
XDS