Использование Task.WhenAll с растущим списком задач - PullRequest
0 голосов
/ 20 мая 2018

Task.WhenAll(IEnumerable<Task>) ожидает завершения всех задач в IEnumerable - но только задач в списке при первом вызове.Если какая-либо активная задача добавляется в список, они не рассматриваются.Этот короткий пример демонстрирует:

    List<Task> _tasks = new List<Task>();

    public async Task  QuickExample()
    {
        for(int n =0; n < 6; ++n)
            _tasks.Add(Func1(n));

        await Task.WhenAll(_tasks);     
        Console.WriteLine("Some Tasks complete");

        await Task.WhenAll(_tasks);
        Console.WriteLine("All Tasks complete");
    }


    async Task Func1(int n)
    {
        Console.WriteLine($"Func1-{n} started");
        await Task.Delay(2000);
        if ((n % 3) == 1)
            _tasks.Add(Func2(n));
        Console.WriteLine($"Func1-{n} complete");
    }

    async Task Func2(int n)
    {
        Console.WriteLine($"Func2-{n} started");
        await Task.Delay(2000);
        Console.WriteLine($"Func2-{n} complete");
    }

Это выводит:

Func1-0 started
Func1-1 started
Func1-2 started
Func1-3 started
Func1-4 started
Func1-5 started
Func1-5 complete
Func1-3 complete
Func2-1 started
Func1-1 complete
Func1-0 complete
Func1-2 complete
Func2-4 started
Func1-4 complete
Some Tasks complete
Func2-4 complete
Func2-1 complete
All Tasks complete
Done

Второй Task.WhenAll() решает проблему в этом случае, но это довольно хрупкое решение.Какой лучший способ справиться с этим в общем случае?

Ответы [ 4 ]

0 голосов
/ 21 мая 2018

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

while (_tasks.Any( t => !t.IsCompleted ) 
{
    await Task.WhenAll(_tasks);
}

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

0 голосов
/ 20 мая 2018

Учтите это;Похоже, работа отправляется в «Список задач» из другого потока.В некотором смысле, сам поток «отправки задач» может также быть еще одной задачей, которую вы должны ждать.

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

Ваша функция ожидания может / должна быть двухэтапной:

  1. Ожиданиедля завершения задачи «Отправка задачи», сигнализирующей, что все задачи отправлены
  2. Дождитесь завершения всех отправленных задач.

Пример:

public async Task WaitForAllSubmittedTasks()
{
    // Work is being submitted in a background thread;
    // Wrap that thread in a Task, and wait for it to complete.
    var workScheduler = GetWorkScheduler();
    await workScheduler;

    // All tasks submitted!

    // Now we get the completed list of all submitted tasks.
    // It's important to note: the submitted tasks
    // have been chugging along all this time.
    // By the time we get here, there are probably a number of
    // completed tasks already.  It does not delay the speed
    // or execution of our work items if we grab the List
    // after some of the work has been completed.
    //
    // It's entirely possible that - by the time we call
    // this function and wait on it - almost all the 
    // tasks have already been completed!
    var submittedWork = GetAllSubmittedTasks();
    await Task.WhenAll(submittedWork);

    // Work complete!
}
0 голосов
/ 21 мая 2018

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

Реализация Task.WhenAll будет повторяться/ копировать задачи в локальный список, поэтому добавленные задачи после вызова Task.WhenAll будут игнорироваться.

В вашем конкретном случае перемещение вызова на Func1 до await Task.Delay() может стать решением.

async Task Func1(int n)
{
    Console.WriteLine($"Func1-{n} started");
    if ((n % 3) == 1)
        _tasks.Add(Func2(n));

    await Task.Delay(2000);
    Console.WriteLine($"Func1-{n} complete");
}

Но если в реальном сценарии вызов Func2 зависит от результата какого-либо асинхронного метода, то вам нужно другое решение.

0 голосов
/ 20 мая 2018

Вы модифицируете List<> без блокировки ... Вам нравится жить опасной жизнью :-) Сохраните Count из _tasks перед выполнением WaitAll, затем после проверки WaitAllCount из _tasks.Если это не так, сделайте еще один раунд (поэтому вам нужно while вокруг WaitAll.

int count = _tasks.Count;

while (true)
{
    await Task.WhenAll(_tasks);

    lock (_tasks)
    {
        if (count == _tasks.Count)
        {
            Console.WriteLine("All Tasks complete");
            break;
        }

        count = _tasks.Count;
        Console.WriteLine("Some Tasks complete");
    }
}

async Task Func1(int n)
{
    Console.WriteLine($"Func1-{n} started");
    await Task.Delay(2000);

    if ((n % 3) == 1)
    {
        lock (_tasks)
        {
            _tasks.Add(Func2(n));
        }
    }

    Console.WriteLine($"Func1-{n} complete");
}

Я добавлю второй (возможно, более правильное решение), который отличается от того, что вывы делаете: вы можете просто await новые Task s из Task s, которые их сгенерировали, без каскадирования их в коллекцию * 1016. * Если A создает B, то A не заканчивается до тех пор, пока не завершится B.Очевидно, вам не нужно добавлять новые Task s в коллекцию _tasks.

...