Ожидание нескольких задач с разными результатами

У меня есть 3 задачи:

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

Все они должны запускаться до того, как мой код может продолжить, и мне также нужны результаты от каждого. Ни один из результатов не имеет ничего общего друг с другом.

Как мне позвонить и ждать выполнения трех задач, а затем получить результаты?

Ответ 1

После использования 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 может вызвать проблемы в других сценариях.

Ответ 2

Просто await три задачи отдельно, после их запуска.

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

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

Ответ 3

Если вы используете С# 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());

Ответ 4

Вы можете сохранить их в задачах, а затем ждать их всех:

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;

Ответ 5

Учитывая три задачи - FeedCat(), SellHouse() и BuyCar(), есть два интересных случая: либо все они выполняются синхронно (по какой-то причине, возможно, кеширование или ошибка), либо нет.

Допустим, у нас есть, из вопроса:

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[]) и списки (внутри). Это работает, но это не великое ИМО. Во многих отношениях проще использовать async операцию и просто await каждой по очереди:

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. с недавним С# общий шаблон для 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<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 для Task и везде для ValueTask<T>

Ответ 6

Вы можете использовать Task.WhenAll, как указано, или Task.WaitAll, в зависимости от того, хотите ли вы, чтобы поток подождал. Взгляните на ссылку для объяснения того и другого.

WaitAll vs WhenAll

Ответ 7

Используйте 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.

Ответ 8

Если вы пытаетесь регистрировать все ошибки, убедитесь, что вы сохранили строку 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. SellHouse уже успешно завершен, когда FeedCat не удалось. В этом случае вы в порядке.

  2. SellHouse не завершен и в какой-то момент завершается с ошибкой. Исключение не наблюдается и будет переброшено в поток финализатора.

  3. SellHouse не является полным и содержит внутри него. В случае, если ваш код работает в ASP.NET, SellHouse завершится неудачно, как только некоторые из них будут завершены. Это происходит из-за того, что вы, в основном, сделали пожар и забываете вызов, и контекст синхронизации был потерян, как только произошел сбой FeedCat.

Вот ошибка, которую вы получите для case (3):

System.AggregateException: A Task 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

Ответ 9

Вперед предупреждение

Просто быстрое знакомство с теми, кто посещает этот и другие подобные потоки, которые ищут способ распараллелить EntityFramework с помощью набора инструментов async + await + task: показанный здесь шаблон является правильным, однако, когда дело доходит до специальной снежинки EF, вы не будете достигайте параллельного выполнения до тех пор, пока вы не используете отдельный (новый) экземпляр db-context внутри каждого и каждого задействованного вызова * Async().

Такого рода вещи необходимы из-за внутренних конструктивных ограничений ef-db-context, которые запрещают выполнять несколько запросов параллельно в одном экземпляре ef-db-context.


Основываясь на уже полученных ответах, это способ убедиться, что вы собираете все значения даже в том случае, если одна или несколько задач приводят к исключению:

  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 ? carTask.Result : (await houseTask);

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