it-roy-ru.com

Выполнение задач параллельно

Итак, в основном у меня есть куча задач (10), и я хочу запустить их все одновременно и ждать, пока они завершатся. По завершении я хочу выполнить другие задачи. Я прочитал кучу ресурсов об этом, но я не могу понять это правильно для моего конкретного случая ...

Вот что у меня сейчас (код был упрощен):

public async Task RunTasks()
{
    var tasks = new List<Task>
    {
        new Task(async () => await DoWork()),
        //and so on with the other 9 similar tasks
    }


    Parallel.ForEach(tasks, task =>
    {
        task.Start();
    });

    Task.WhenAll(tasks).ContinueWith(done =>
    {
        //Run the other tasks
    });
}

//This function perform some I/O operations
public async Task DoWork()
{
    var results = await GetDataFromDatabaseAsync();
    foreach (var result in results)
    {
        await ReadFromNetwork(result.Url);
    }
}

Так что моя проблема в том, что когда я жду, когда задачи завершатся с вызовом WhenAll, он говорит мне, что все задачи завершены, даже если ни одна из них не завершена. Я попытался добавить Console.WriteLine в мою foreach, и когда я вошел в задачу продолжения, данные продолжают поступать из моих предыдущих Tasks, которые еще не завершены.

Что я здесь не так делаю?

21
Yann Thibodeau

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

Вы можете просто вызвать DoWork и вернуть задание, сохранить его в списке и дождаться завершения всех заданий. Имея в виду:

tasks.Add(DoWork());
// ...
await Task.WhenAll(tasks);

Однако асинхронные методы работают синхронно, пока не будет достигнуто первое ожидание незавершенной задачи. Если вы беспокоитесь о том, что эта часть занимает слишком много времени, используйте Task.Run, чтобы перенести ее в другой поток ThreadPool, а затем сохраните that task в списке:

tasks.Add(Task.Run(() => DoWork()));
// ...
await Task.WhenAll(tasks);
23
i3arnon

По сути, вы смешиваете две несовместимые асинхронные парадигмы; т.е. Parallel.ForEach() и async-await.

Для чего хочешь, делай одно или другое. Например. Вы можете просто использовать Parallel.For[Each]() и вообще отказаться от асинхронного ожидания. Функция Parallel.For[Each]() вернется только после завершения всех параллельных задач, после чего вы сможете перейти к другим задачам.

В коде есть и другие проблемы:

  • вы помечаете метод как асинхронный, но не ожидаете его (ожидание, которое у вас есть, находится в делегате, а не в методе);

  • вы почти наверняка хотите, чтобы .ConfigureAwait(false) ожидала вас, особенно если вы не пытаетесь сразу использовать результаты в потоке пользовательского интерфейса.

8
sellotape

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

public async Task RunTasks()
{
    var tasks = new List<Func<Task>>
    {
       DoWork,
       //...
    };

    await Task.WhenAll(tasks.AsParallel().Select(async task => await task()));

    //Run the other tasks
}

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

Для большинства случаев лучшим способом будет:

public async Task RunTasks()
{
    await Task.WhenAll(new [] 
    {
        DoWork(),
        //...
    });
    //Run the other tasks
}

На мой взгляд в вашем коде:

  1. Вы не должны переносить код в Task перед тем, как перейти к Parallel.ForEach.

  2. Вы можете просто awaitTask.WhenAll вместо использования ContinueWith.

2
Andrey Tretyak

Метод DoWork является методом асинхронного ввода-вывода. Это означает, что вам не нужно несколько потоков для выполнения нескольких из них, так как большую часть времени метод будет асинхронно ожидать завершения ввода-вывода. Одного потока достаточно, чтобы сделать это.

public async Task RunTasks()
{
    var tasks = new List<Task>
    {
        DoWork(),
        //and so on with the other 9 similar tasks
    };

    await Task.WhenAll(tasks);

    //Run the other tasks            
}

Вы должны почти никогда не использовать конструктор Task для создания новой задачи. Чтобы создать задачу асинхронного ввода-вывода, просто вызовите метод async. Чтобы создать задачу, которая будет выполняться в потоке пула потоков, используйте Task.Run. Вы можете прочитать эту статью для подробного объяснения Task.Run и других вариантов создания задач.

1
Jakub Lortz

Просто добавьте блок try-catch вокруг Task.WhenAll

Примечание: создается экземпляр System.AggregateException, который действует как оболочка для одного или нескольких возникших исключений. Это важно для методов, которые координируют несколько задач, таких как Task.WaitAll () и Task.WaitAny (), так что AggregateException может обернуть все исключения в запущенных задачах, которые произошли.

try
{ 
    Task.WaitAll(tasks.ToArray());  
}
catch(AggregateException ex)
{ 
    foreach (Exception inner in ex.InnerExceptions)
    {
    Console.WriteLine(String.Format("Exception type {0} from {1}", inner.GetType(), inner.Source));
    }
}
0
NiTRiX-Reloaded