Очікування декількох завдань з різними результатами


237

У мене є 3 завдання:

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

Всі вони повинні працювати, перш ніж мій код може продовжуватися, і мені потрібні також результати від кожного. Жоден із результатів не має нічого спільного між собою

Як мені зателефонувати і чекати, коли 3 завдання будуть виконані, а потім отримати результати?


25
Чи є якісь вимоги щодо замовлення? Тобто, ви хочете не продавати будинок до тих пір, поки кішку не нагодують?
Ерік Ліпперт

Відповіді:


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з цього цілком; очікуючий піклується про те, щоб ви не пройшли повз 3 пізніших завдання, поки завдання не будуть виконані.
Серві

134
Task.WhenAll()дозволяє виконувати завдання в паралельному режимі. Я не можу зрозуміти, чому @Servy запропонував її видалити. Без WhenAllних вони будуть бігати по одному
Сергій Г.

87
@Sergey: Завдання починають виконувати негайно. Напр., catTaskВже працює до моменту повернення FeedCat. Тож будь-який підхід спрацює - питання лише в тому, чи хочете ви awaitїх один за одним або всі разом. Поводження з помилками дещо відрізняється - якщо ви користуєтеся Task.WhenAll, то це дозволить awaitїм усе, навіть якщо одна з них виходить з ладу рано.
Стівен Клірі

23
@Sergey Calling WhenAllне впливає на те, коли виконуються операції чи як вони виконуються. У ньому є лише будь-яка можливість впливати на спостереження результатів. У цьому конкретному випадку єдина відмінність полягає в тому, що помилка в одному з перших двох методів призведе до того, що виняток буде кинуто в цей стек викликів раніше в моєму методі, ніж у Стивена (хоча та сама помилка завжди буде викинута, якщо є якісь ).
Сервіс

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;

8
@Bargitta Ні, це неправда. Вони будуть робити свою роботу паралельно. Сміливо запускайте його і переконайтеся самі.
Сервіс

5
Люди продовжують задавати одне і те ж питання через роки ... Я вважаю, що важливо знову наголосити на тому, що завдання " починається створювати " в тілі відповіді : можливо, вони не турбують читання коментарів

9
@StephenYork Додавання Task.WhenAllзмін буквально нічого не стосується поведінки програми, будь-яким способом, що спостерігається. Це суто зайвий виклик методу. Ви можете додавати його, якщо хочете, як естетичний вибір, але це не змінює те, що робить код. Час виконання коду буде ідентичним або без виклику цього методу (ну, технічно, для дзвінків дійсно невеликі накладні витрати WhenAll, але це повинно бути незначним), лише зробивши цю версію трохи довшою, ніж ця версія.
Сервіс

4
@StephenYork Ваш приклад виконує операції послідовно з двох причин. Ваші асинхронні методи насправді не асинхронні, вони синхронні. Той факт, що у вас є синхронні методи, які завжди повертають вже виконані завдання, заважає виконувати їх одночасно. Далі ви насправді не виконуєте те, що показано у цій відповіді, запускаючи всі три асинхронні методи, а потім чекаючи на три завдання по черзі. Ваш приклад не викликає кожен метод до тих пір, поки попередній не закінчиться, тим самим явно не дозволяючи його запустити до завершення попереднього, на відміну від цього коду.
Сервіс

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 та вже виконаних завдань, якщо ви маєте намір перетворити цей приклад на щось справжнє.


Тут є єдиною функцією C # 7. Вони, безумовно, у фінальному випуску.
Джоель Мюллер

Я знаю про кортежі та c # 7. Я маю на увазі, що не можу знайти метод WhenAll, який повертає кортежі. Який простір імен / пакет?
Юрій Щербаков

@YuryShcherbakov Task.WhenAll()не повертає кортеж. Один будується із Resultвластивостей наданих завдань після повернення завдання, Task.WhenAll()завершеного.
Кріс Чарабарук

2
Я б запропонував замінити .Resultдзвінки відповідно до міркувань Стівена, щоб уникнути інших людей, що продовжують погану практику, копіюючи ваш приклад.
julealgon

Цікаво, чому цей метод не є цією частиною основи? Це здається настільки корисним. У них не вистачало часу і доведеться зупинятися на одному типі повернення?
Ян

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>


"Всупереч різноманітним відповідям тут, використовуючи функцію очікування замість Завдання. Коли всі значення не мають значення в тому, як виконуються завдання (паралельно, послідовно тощо)", я не бачу жодної відповіді, яка б це сказала. Я б уже прокоментував їх, сказавши так само, якби вони зробили. Є багато коментарів до багатьох відповідей, які говорять про це, але немає відповідей. На кого ви звертаєтесь? Також зауважте, що ваша відповідь не відповідає результатам завдань (або не стосується того, що результати є різного типу). Ви склали їх методом, який просто повертає a, Taskколи вони все виконано, не використовуючи результати.
Сервіс

@Servy ви праві, це були коментарі;
Додаю

@Servy tweak додано
Marc Gravell

Крім того, якщо ви збираєтеся скористатись ранньою передачею синхронних завдань, ви можете також обробляти будь-які завдання, синхронно скасовувані або несправні, а не лише ті, які успішно виконані. Якщо ви вирішили, що це оптимізація, яка потрібна вашій програмі (що буде рідко, але трапиться), то ви також можете пройти весь шлях.
Сервіс

@ Сервіс, що є складною темою - ви отримуєте різну семантику винятку з двох сценаріїв - очікуючи, що викликати виняток, поводиться інакше, ніж доступ до .Result, щоб викликати виняток. ІМО в цей момент нам слід 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 ... також дивіться відповідь Серві.
Росді Касім

1
якщо мені потрібно додати .ConfigrtueAwait (false). Чи додавав би я його лише до Task.WhenAll або до кожного очікуваного, який випливає?
AstroSharp

@AstroSharp взагалі, це гарна ідея додати їх до всіх (якщо перший завершено, він фактично ігнорується), але в цьому випадку, мабуть, буде добре просто зробити перше - якщо тільки немає більше асинхронізації речі трапляються пізніше.
Рід Копсей

6

Якщо ви намагаєтесь увімкнути всі помилки, переконайтеся, що ви зберігаєте рядок 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 не вдався.

Ось помилка, яку ви отримаєте для випадку (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


2

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

WaitAll vs 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.

мм ... не Task.Value (можливо, він існував у 2013 році?), а не tCat.Result, tHouse.Result або tCar.Result
Стівен Йорк

1

Вперед Попередження

Просто швидке головування для тих, хто відвідує цю та інші подібні теми, які шукають способу паралелізації EntityFramework за допомогою async + wait + set tool set : Зображення, показане тут, звучить, однак, якщо мова йде про спеціальну сніжинку EF, ви не будете досягти паралельного виконання, якщо і поки ви не використовуєте окремий (новий) екземпляр db-контексту всередині кожного включеного дзвінка * Async ().

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


Використовуючи вже надані відповіді, це спосіб переконатись у тому, що ви збираєте всі значення навіть у тому випадку, якщо одне або кілька завдань призводять до винятку:

  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
У цьому є лише одна проблема: dynamicчи чорт. Це для складного COM interop та такого, і його не слід використовувати в будь-яких ситуаціях, коли це абсолютно не потрібно. Особливо, якщо ви дбаєте про продуктивність. Або тип безпеки. Або рефакторинг. Або налагодження.
Джоель Мюллер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.