Чому я віддаю перевагу синглу "Очікуйте завдання. Коли все" над кількома очікуваннями?


127

У разі, якщо я не дбаю про порядок виконання завдань і просто потрібно, щоб вони все виконали, чи потрібно все-таки використовувати await Task.WhenAllзамість кількох await? наприклад, DoWork2нижче кращого способу DoWork1(і чому?):

using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static async Task<string> DoTaskAsync(string name, int timeout)
        {
            var start = DateTime.Now;
            Console.WriteLine("Enter {0}, {1}", name, timeout);
            await Task.Delay(timeout);
            Console.WriteLine("Exit {0}, {1}", name, (DateTime.Now - start).TotalMilliseconds);
            return name;
        }

        static async Task DoWork1()
        {
            var t1 = DoTaskAsync("t1.1", 3000);
            var t2 = DoTaskAsync("t1.2", 2000);
            var t3 = DoTaskAsync("t1.3", 1000);

            await t1; await t2; await t3;

            Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }

        static async Task DoWork2()
        {
            var t1 = DoTaskAsync("t2.1", 3000);
            var t2 = DoTaskAsync("t2.2", 2000);
            var t3 = DoTaskAsync("t2.3", 1000);

            await Task.WhenAll(t1, t2, t3);

            Console.WriteLine("DoWork2 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }


        static void Main(string[] args)
        {
            Task.WhenAll(DoWork1(), DoWork2()).Wait();
        }
    }
}

2
Що робити, якщо ви насправді не знаєте, скільки завдань вам потрібно виконати паралельно? Що робити, якщо вам потрібно виконати 1000 завдань? Перший буде не дуже читабельним await t1; await t2; ....; await tn=> другий завжди найкращий вибір в обох випадках
cuongle

Ваш коментар має сенс. Я просто намагався щось уточнити для себе, пов'язане з іншим питанням, на яке я нещодавно відповів . У тому випадку було 3 завдання.
авоа

Відповіді:


113

Так, використовувати, WhenAllтому що він поширює всі помилки одночасно. При множині очікувань ви втрачаєте помилки, якщо один з попередніх очікує кидків.

Ще одна важлива відмінність полягає в тому WhenAll, що чекатиме завершення всіх завдань навіть за наявності відмов (несправних або скасованих завдань). Чекання вручну послідовно призведе до несподіваної одночасності, тому що частина вашої програми, яка хоче почекати, насправді триватиме рано.

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


9
"Тому що він поширює всі помилки одразу" Не якщо ви awaitйого результат.
svick

2
Що стосується питання про те, як керувати винятками із Завданням, ця стаття дає швидке, але добре уявлення про міркування, що стоять за нею (і це просто так трапляється, щоб також зробити попередню примітку про переваги WhenAll на відміну від кількох очікувань): блоги .msdn.com / b / pfxteam / archive / 2011/09/28 / 10217876.aspx
Оскар Ліндберг

5
@OskarLindberg ОП починає всі завдання, перш ніж він чекає першого. Тож вони бігають одночасно. Дякуємо за посилання
usr

3
@usr Мені було цікаво все-таки дізнатися, чи IfAll не робить розумних речей, як збереження того ж SynchronizationContext, щоб надалі відсунути його переваги вбік від семантики. Я не знайшов переконливої ​​документації, але, дивлячись на ІЛ, явно є різні реалізації IAsyncStateMachine в грі. Я не так добре читаю ІЛ, але, як мінімум, коли все виглядає, коли генерується більш ефективний код IL. (У будь-якому випадку, лише той факт, що результат WhenAll відображає стан усіх завдань, що займаються мною, є достатньою причиною, щоб віддавати перевагу в більшості випадків.)
Оскар Ліндберг

17
Ще одна важлива відмінність полягає в тому, що WhenAll чекатиме завершення всіх завдань, навіть якщо, наприклад, t1 або t2 кидає виняток або скасовується.
Магнус

28

Я розумію, що основна причина віддавати перевагу Task.WhenAllдекільком awaits - це продуктивність / завдання "набивання": DoWork1метод робить щось подібне:

  • Почніть з заданого контексту
  • збережіть контекст
  • чекати t1
  • відновити вихідний контекст
  • збережіть контекст
  • чекати t2
  • відновити вихідний контекст
  • збережіть контекст
  • чекати t3
  • відновити вихідний контекст

На відміну від DoWork2цього:

  • Почніть з заданого контексту
  • збережіть контекст
  • дочекайтеся всіх t1, t2 і t3
  • відновити вихідний контекст

Зрозуміло, що це досить велика угода для вашого конкретного випадку, це, звичайно, "залежно від контексту" (вибачте за каламбур).


4
Ви, здається, думаєте, що надсилати повідомлення до контексту синхронізації дорого. Це насправді немає. У вас є делегат, який додається до черги, ця чергу буде прочитана і делегат виконаний. Витрати, які це додає, чесно дуже малі. Це не нічого, але і не велике. Витрати будь-яких операцій з асинхронізацією будуть карликувати такі накладні витрати майже у всіх випадках.
Сервіс

Погодився, це була лише єдина причина, яку я міг думати, щоб віддати перевагу одному перед іншим. Ну, і плюс подібність з Task.WaitAll, де комутація потоків - більш значна вартість.
Марсель Попеску

1
@ Сервіс Як Марсель зазначає, що РЕАЛЬНО залежить. Якщо ви, наприклад, використовуєте функцію очікування для всіх завдань db, і цей db сидить на тій же машині, що й екземпляр asp.net, є випадки, коли ви очікуєте удару db, який є в пам'яті індексом, дешевше ніж перемикач синхронізації та переміщення каналів. У такому сценарії може бути вагома загальна виграш при команді WhenAll (), тому ... це дійсно залежить.
Кріс Москіні

3
@ChrisMoschini Неможливо, що запит БД, навіть якщо він потрапляє до БД, що сидить на тій же машині, що і сервер, буде швидше, ніж накладні витрати, додавши до помпа повідомлення кілька делегатів. Цей запит у пам'яті все ще майже напевно буде набагато повільніше.
Сервіс

Також зауважте, що якщо t1 повільніше, а t2 і t3 - швидше - тоді інший чекає повернення негайно.
Девід Рефаелі

18

Асинхронний метод реалізований як стан-машина. Можна записати методи так, щоб вони не були зібрані в стани-машини, це часто називають швидким методом асинхронізації. Вони можуть бути реалізовані так:

public Task DoSomethingAsync()
{
    return DoSomethingElseAsync();
}

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

public Task DoSomethingAsync()
{
    var t1 = DoTaskAsync("t2.1", 3000);
    var t2 = DoTaskAsync("t2.2", 2000);
    var t3 = DoTaskAsync("t2.3", 1000);

    return Task.WhenAll(t1, t2, t3);
}

7

(Відмова: Ця відповідь взята / натхнена курсом TPL Async IPL Griffiths з Pluralsight )

Ще одна причина, коли слід віддавати перевагу WhenAll - це обробка винятків.

Припустимо, у вас був метод пробного захоплення методів DoWork, і припустимо, що вони викликали різні методи DoTask:

static async Task DoWork1() // modified with try-catch
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        await t1; await t2; await t3;

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
    }
    catch (Exception x)
    {
        // ...
    }

}

У цьому випадку, якщо всі 3 завдання кинуть винятки, буде спіймано лише перше. Будь-який наступний виняток буде втрачено. Тобто, якщо t2 і t3 кидає виняток, буде вилучено лише t2; Подальші винятки із завдань залишаться непоміченими.

Де як у програмі WhenAll - якщо виникла будь-яка або всі завдання, отримане завдання буде містити всі винятки. Ключове слово, яке очікує, все ще повторно кидає перший виняток. Тож інші винятки все ще фактично не помічені. Один із способів подолати це - додати порожнє продовження після завдання WhenAll і покласти туди очікування. Таким чином, якщо завдання не вдасться, властивість результату викине повне виняткове сукупність:

static async Task DoWork2() //modified to catch all exceptions
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        var t = Task.WhenAll(t1, t2, t3);
        await t.ContinueWith(x => { });

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t.Result[0], t.Result[1], t.Result[2]));
    }
    catch (Exception x)
    {
        // ...
    }
}

6

Інші відповіді на це питання пропонують технічні причини, чому await Task.WhenAll(t1, t2, t3);віддається перевага. Ця відповідь буде спрямована на те, щоб поглянути на неї з більш м'якої сторони (на яку @usr натякає), при цьому все ж таки дійти до того ж висновку.

await Task.WhenAll(t1, t2, t3); є більш функціональним підходом, оскільки він заявляє про наміри і є атомним.

З await t1; await t2; await t3;цим, ніщо не заважає товаришеві по команді (а може бути, і вашому майбутньому самому!) Додавати код між особоюawait заявами. Звичайно, ви стиснули його в один рядок, щоб по суті це зробити, але це не вирішує проблему. Крім того, це загально погана форма в командному налаштуванні включати кілька висловлювань у заданий рядок коду, оскільки це може ускладнити сканування вихідного файлу для людських очей.

Простіше кажучи, await Task.WhenAll(t1, t2, t3);є більш рентабельним, оскільки він чіткіше повідомляє про ваш намір і менш вразливий до своєрідних помилок, які можуть вийти з цілеспрямованих оновлень коду або навіть просто злитися, пішли неправильно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.