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

237

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

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

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

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

Ян Винк
источник
25
Есть ли у вас какие-либо требования к оформлению заказа? То есть вы не хотите продавать дом до тех пор, пока кошка не накормится?
Эрик Липперт

Ответы:

412

После использования 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из этого полностью; ожидающие позаботятся о том, чтобы вы не прошли 3 более поздних задания, пока все задачи не будут выполнены.
Servy
134
Task.WhenAll()позволяет запустить задачу в параллельном режиме. Я не могу понять, почему @Servy предложил удалить его. Без WhenAllних они будут бегать один за другим
Сергей Григорьевич
87
@ Сергей: задачи начинают выполняться немедленно. Например, catTaskон уже запущен к тому времени, как вернулся FeedCat. Таким образом, любой подход будет работать - единственный вопрос, хотите ли вы, чтобы awaitони по одному или все вместе. Обработка ошибок немного отличается - если вы используете Task.WhenAll, то они будут awaitвсе, даже если один из них рано выходит из строя.
Стивен Клири
23
@Sergey Calling WhenAllне влияет на то, когда выполняются операции или как они выполняются. У него есть только возможность повлиять на то, как наблюдаются результаты. В этом конкретном случае единственное отличие состоит в том, что ошибка в одном из первых двух методов привела бы к тому, что исключение было выдано в этот стек вызовов раньше в моем методе, чем у Стивена (хотя одна и та же ошибка всегда будет выдаваться, если есть какие-либо ).
Serv
37
@Sergey: ключ в том, что асинхронные методы всегда возвращают «горячие» (уже запущенные) задачи.
Стивен Клири
99

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

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

var cat = await catTask;
var house = await houseTask;
var car = await carTask;
Servy
источник
8
@ Баргитта Нет, это неправда. Они будут выполнять свою работу параллельно. Не стесняйтесь запустить его и убедитесь сами.
Обслуживание
5
Люди продолжают задавать один и тот же вопрос спустя годы ... Я чувствую, что важно еще раз подчеркнуть, что задание « начинается с создания » в теле ответа : возможно, они не утруждают себя чтением комментариев
9
@StephenYork Добавление Task.WhenAllбуквально ничего не меняет в поведении программы любым видимым способом. Это чисто избыточный вызов метода. Вы можете добавить его, если хотите, в качестве эстетического выбора, но это не меняет действия кода. Время выполнения кода будет одинаковым как с вызовом этого метода, так и без него (ну, технически это приведет к очень небольшим издержкам при вызове WhenAll, но это должно быть незначительным), только делая эту версию немного более длинной для запуска, чем эта версия.
Servy
4
@StephenYork Ваш пример запускает операции последовательно по двум причинам. Ваши асинхронные методы на самом деле не асинхронные, а синхронные. Тот факт, что у вас есть синхронные методы, которые всегда возвращают уже выполненные задачи, не позволяет им работать одновременно. Далее, вы на самом деле не делаете то, что показано в этом ответе, запуская все три асинхронных метода, а затем ожидая три задачи по очереди. Ваш пример не вызывает каждый метод до тех пор, пока предыдущий не закончил, таким образом явно не позволяя запустить один метод, пока предыдущий не закончил, в отличие от этого кода.
Обслуживание
4
@MarcvanNieuwenhuijzen Это явно не соответствует действительности, как обсуждалось в комментариях здесь и других ответах. Добавление WhenAll- это чисто эстетическое изменение. Единственное заметное различие в поведении заключается в том, ожидаете ли вы, когда более поздние задачи завершатся в случае сбоя более ранней задачи, что обычно не требуется. Если вы не верите многочисленным объяснениям того, почему ваше утверждение неверно, вы можете просто запустить код для себя и убедиться, что оно неверно.
Servy
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 и уже выполненных задач, если вы собираетесь превратить этот пример в нечто реальное.

Джоэл Мюллер
источник
Кортежи - единственная особенность C # 7, участвующая здесь. Это определенно в финальной версии.
Джоэл Мюллер,
Я знаю о кортежах и c # 7. Я имею в виду, я не могу найти метод WhenAll, который возвращает кортежи. Какое пространство имен / пакет?
Юрий Щербаков
@YuryShcherbakov Task.WhenAll()не возвращает кортеж. Один создается из Resultсвойств предоставленных задач после того, как задача, возвращенная Task.WhenAll()завершением.
Крис Чарабарук
2
Я бы предложил заменить .Resultзвонки согласно рассуждениям Стивена, чтобы другие люди не увековечивали плохую практику, копируя ваш пример.
Jullealgon
Интересно, почему этот метод не является частью фреймворка? Это кажется таким полезным. У них не хватило времени и нужно было остановиться на одном типе возврата?
Ян Грейнджер
14

Даны три задачи - 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. в недавнем 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 для Taskи везде дляValueTask<T>

Марк Гравелл
источник
«Вопреки различным ответам здесь, используя await вместо Task.WhenAll не имеет никакого значения для выполнения задач (одновременно, последовательно и т. Д.)» Я не вижу никакого ответа, который говорит это. Я бы уже прокомментировал, как они говорят, если бы они это сделали. Есть много комментариев на множество ответов, говорящих это, но нет ответов. Что вы имеете в виду? Также обратите внимание, что ваш ответ не обрабатывает результат заданий (или имеет дело с тем фактом, что все результаты другого типа). Вы скомпоновали их в методе, который просто возвращает a, Taskкогда все они сделаны без использования результатов.
Servy
@ Служите, вы правы, это были комментарии; Я добавлю твик, чтобы показать, используя результаты
Марк Гравелл
Добавлен твик @Servy
Марк Гравелл
Кроме того, если вы собираетесь отказаться от выполнения синхронных задач на раннем этапе, вы также можете обрабатывать любые задачи, синхронно отменяемые или сбойные, а не только успешно выполненные. Если вы приняли решение, что ваша программа нуждается в оптимизации (которая будет редкой, но будет иметь место), то вы также можете пройти весь путь.
Servy
@ Служба, которая является сложной темой - вы получаете различную семантику исключений из двух сценариев - ожидание, чтобы вызвать исключение, ведет себя по-другому, чем доступ. Результат для запуска исключения. ИМО на тот момент мы должныawait получить «лучшую» семантику исключений, исходя из предположения, что исключения являются редкими, но значимыми
Марк Грэвелл
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()часть становится бесполезной, поскольку метод уже выполнен?
Краанг Прайм
1
@sanuel, если они возвращают задание <t>, то нет ... они запускают асинхронное открытие, но не ждут его
Рид Копси,
Я не думаю, что это точно, пожалуйста, посмотрите обсуждения под ответом @ StephenCleary ... также посмотрите ответ Servy.
Росди Касим
1
если мне нужно добавить .ConfigrtueAwait (false). Могу ли я добавить его только к Task.WhenAll или к каждому последующему ожидающему?
AstroSharp
@AstroSharp в целом, это хорошая идея, чтобы добавить его ко всем из них (если первый завершен, он фактически игнорируется), но в этом случае, вероятно, будет хорошо просто сделать первый - если не будет больше асинхронных вещи происходят позже.
Рид Копси,
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. SellHouse уже успешно завершен, когда FeedCat не удалось. В этом случае вы в порядке.

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

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

Вот ошибка, которую вы получите для case (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

samfromlv
источник
2

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

WaitAll vs WhenAll

christiandev
источник
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.
It'sNotALie.
источник
мм ... не Task.Value (может быть, он существовал в 2013 году?), скорее tCat.Result, tHouse.Result или tCar.Result
Стивен Йорк,
1

Предупреждение вперед

Просто быстрый заголовок для тех, кто посещает этот и другие подобные потоки, которые ищут способ распараллелить 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 ? houseTask.Result : (await houseTask);

        return DoSomething(cat, car, house);
     }
 }
XDS
источник
-1
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

если вы хотите получить доступ к Cat, вы делаете это:

var ct = (Cat)dn[0];

Это очень просто сделать и очень полезно использовать, нет необходимости искать сложное решение.

taurius
источник
1
Есть только одна проблема с этим: dynamicдьявол. Это для сложного взаимодействия COM и тому подобное, и не должно использоваться в любой ситуации, где это не является абсолютно необходимым. Особенно, если вы заботитесь о производительности. Или типа безопасность. Или рефакторинг. Или отладка.
Джоэл Мюллер