Запуск нескольких асинхронных задач и ожидание их завершения

265

Мне нужно запустить несколько асинхронных задач в консольном приложении и дождаться их завершения перед дальнейшей обработкой.

Есть много статей, но я, кажется, растерялся, когда больше читаю. Я прочитал и понял основные принципы библиотеки задач, но мне явно не хватает ссылки где-то.

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

Какая простейшая реализация для такого сценария?

Даниэль Миннаар
источник

Ответы:

442

В обоих ответах не упоминалось ожидаемое Task.WhenAll:

var task1 = DoWorkAsync();
var task2 = DoMoreWorkAsync();

await Task.WhenAll(task1, task2);

Основное различие между Task.WaitAllи Task.WhenAllзаключается в том, что первый будет блокировать (аналогично использованиюWait в одной задаче), а последний не будет и его можно ожидать, передавая управление вызывающей стороне до завершения всех задач.

Более того, обработка исключений отличается:

Task.WaitAll:

По крайней мере, один из экземпляров Задачи был отменен или возникла исключительная ситуация при выполнении хотя бы одного из экземпляров Задачи. Если задача была отменена, AggregateException содержит OperationCanceledException в своей коллекции InnerExceptions.

Task.WhenAll:

Если любая из предоставленных задач завершается в состоянии Failed, возвращаемая задача также завершается в состоянии Failed, где ее исключения будут содержать агрегацию набора развернутых исключений для каждой из предоставленных задач.

Если ни одна из поставленных задач не была выполнена, но хотя бы одна из них была отменена, возвращенная задача завершится в состоянии Отменено.

Если ни одна из задач не была выполнена, и ни одна из задач не была отменена, результирующая задача завершится в состоянии RanToCompletion. Если предоставленный массив / enumerable не содержит задач, возвращаемая задача немедленно перейдет в состояние RanToCompletion, прежде чем она будет возвращена вызывающей стороне.

Ювал Ицчаков
источник
4
Когда я пытаюсь это сделать, мои задачи выполняются последовательно? Нужно ли начинать каждую задачу отдельно await Task.WhenAll(task1, task2);?
Запнологика
4
@Zapnologica Task.WhenAllне запускает задачи для вас. Вы должны предоставить им «горячий», то есть уже началось.
Юваль Ицчаков
2
Хорошо. В этом есть смысл. Так что же сделает ваш пример? Потому что вы их не начали?
Запнологика
2
@YuvalItzchakov большое спасибо! Это так просто, но это мне очень помогло сегодня! Стоит минимум +1000 :)
Даниэль Душек
1
@Pierre Я не следую. Какое отношение StartNewимеют новые задачи к асинхронному ожиданию их всех?
Ювал Ицчаков
106

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

List<Task> TaskList = new List<Task>();
foreach(...)
{
   var LastTask = new Task(SomeFunction);
   LastTask.Start();
   TaskList.Add(LastTask);
}

Task.WaitAll(TaskList.ToArray());
Вирус
источник
48
Я бы порекомендовал WhenAll
Ravi
Можно ли запустить несколько новых потоков одновременно, используя ключевое слово await, а не .Start ()?
Мэтт W
1
@ MattW Нет, когда вы используете await, он будет ждать его завершения. В этом случае вы не сможете создать многопоточную среду. Это причина того, что все задачи ожидаются в конце цикла.
Вирус
5
Downvote для будущих читателей, так как не ясно, что это блокирующий вызов.
JRoughan
Смотрите принятый ответ по причинам, почему бы не сделать это.
EL MOJO
26

Лучший вариант, который я видел, это следующий метод расширения:

public static Task ForEachAsync<T>(this IEnumerable<T> sequence, Func<T, Task> action) {
    return Task.WhenAll(sequence.Select(action));
}

Назовите это так:

await sequence.ForEachAsync(item => item.SomethingAsync(blah));

Или с асинхронной лямбдой:

await sequence.ForEachAsync(async item => {
    var more = await GetMoreAsync(item);
    await more.FrobbleAsync();
});
me22
источник
26

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

введите описание изображения здесь

пример

var tasks = new Task[] {
    TaskOperationOne(),
    TaskOperationTwo()
};

Task.WaitAll(tasks);
// or
await Task.WhenAll(tasks);

Если вы хотите выполнять задачи в определенном порядке, вы можете получить вдохновение от этого ответа.

NtFreX
источник
извините за опоздание на вечеринку, но, почему у вас есть awaitдля каждой операции и в то же время использовать WaitAllили WhenAll. Не должны ли задачи при Task[]инициализации быть без await?
Ди ZG
@dee zg Вы правы. Ожидание выше побеждает цель. Я изменю свой ответ и удалю их.
NtFreX
да это оно. Спасибо за разъяснение! (upvote за хороший ответ)
dee zg
8

Вы хотите соединить цепочки Taskили они могут быть вызваны параллельно?

Для цепочки
Просто сделай что-то вроде

Task.Run(...).ContinueWith(...).ContinueWith(...).ContinueWith(...);
Task.Factory.StartNew(...).ContinueWith(...).ContinueWith(...).ContinueWith(...);

и не забудьте проверить предыдущий Taskэкземпляр в каждом, так ContinueWithкак он может быть неисправен.

Для параллельного подхода
Самый простой метод, с которым я сталкивался: в Parallel.Invoke противном случае есть Task.WaitAllили вы даже можете использовать WaitHandles для обратного отсчета до нуля оставленных действий (подождите, есть новый класс:) CountdownEvent, или ...

Андреас Нидермаир
источник
3
Ценю ответ, но ваши предложения могли бы быть объяснены немного больше.
Даниэль Миннаар
@drminnaar, какое еще объяснение, кроме ссылок на msdn с примерами, вам нужно? Вы даже не нажимали на ссылки, не так ли?
Андреас Нидермаир
4
Я нажал на ссылки, и я прочитал содержание. Я собирался на Invoke, но было много «если» и «но» о том, работает ли он асинхронно или нет. Вы редактировали свой ответ постоянно. Ссылка на WaitAll, которую вы разместили, была идеальной, но я нашел ответ, который продемонстрировал ту же функциональность в более быстром и удобном для чтения виде. Не обижайтесь, ваш ответ по-прежнему предоставляет хорошие альтернативы другим подходам.
Даниэль Миннаар
@drminnaar здесь без обид, мне просто любопытно :)
Андреас Нидермайр
5

Вот как я это делаю с массивом Func <> :

var tasks = new Func<Task>[]
{
   () => myAsyncWork1(),
   () => myAsyncWork2(),
   () => myAsyncWork3()
};

await Task.WhenAll(tasks.Select(task => task()).ToArray()); //Async    
Task.WaitAll(tasks.Select(task => task()).ToArray()); //Or use WaitAll for Sync
DalSoft
источник
1
Почему бы вам просто не сохранить его как массив задач?
Талха Талип Ачыкгез
1
Если вы не внимательны, @ talha-talip-açıkgöz, вы выполняете задачи, когда не ожидали их выполнения. Выполнение этого в качестве делегата Func прояснит ваше намерение.
DalSoft
5

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

var cats = new List<Cat>();
var dog = new Dog();

var loadDataTasks = new Task[]
{
    Task.Run(async () => cats = await LoadCatsAsync()),
    Task.Run(async () => dog = await LoadDogAsync())
};

try
{
    await Task.WhenAll(loadDataTasks);
}
catch (Exception ex)
{
    // handle exception
}
Егор Хромадский
источник
1
Если LoadCatsAsync()и LoadDogAsync()являются просто вызовами базы данных, они связаны с IO. Task.Run()для работы с процессором; это добавляет дополнительные ненужные издержки, если все, что вы делаете, - это ожидаете ответа от сервера базы данных. Принятый ответ Ювала - правильный путь для работы, связанной с IO.
Стивен Кеннеди
@StephenKennedy Не могли бы вы уточнить, какие накладные расходы и как они могут повлиять на производительность? Спасибо!
Егор
Это было бы довольно сложно подвести итог в поле для комментариев :) Вместо этого я рекомендую прочитать статьи Стивена Клири - он эксперт в этом деле. Начните здесь: blog.stephencleary.com/2013/10/…
Стивен Кеннеди
-1

Я подготовил фрагмент кода, чтобы показать вам, как использовать задачу для некоторых из этих сценариев.

    // method to run tasks in a parallel 
    public async Task RunMultipleTaskParallel(Task[] tasks) {

        await Task.WhenAll(tasks);
    }
    // methode to run task one by one 
    public async Task RunMultipleTaskOneByOne(Task[] tasks)
    {
        for (int i = 0; i < tasks.Length - 1; i++)
            await tasks[i];
    }
    // method to run i task in parallel 
    public async Task RunMultipleTaskParallel(Task[] tasks, int i)
    {
        var countTask = tasks.Length;
        var remainTasks = 0;
        do
        {
            int toTake = (countTask < i) ? countTask : i;
            var limitedTasks = tasks.Skip(remainTasks)
                                    .Take(toTake);
            remainTasks += toTake;
            await RunMultipleTaskParallel(limitedTasks.ToArray());
        } while (remainTasks < countTask);
    }
Сая Имад
источник
1
как получить результаты задач? Например, для слияния «строк» ​​(из N задач параллельно) в таблицу данных и привязку ее к gridview asp.net?
PreguntonCojoneroCabrón