Como usar o await em um loop

86

Estou tentando criar um aplicativo de console assíncrono que faz algum trabalho em uma coleção. Eu tenho uma versão que usa paralelo para loop outra versão que usa async / await. Eu esperava que a versão assíncrona / aguardar funcionasse de forma semelhante à versão paralela, mas executa de forma síncrona. O que estou fazendo errado?

class Program
{
    static void Main(string[] args)
    {
        var worker = new Worker();
        worker.ParallelInit();
        var t = worker.Init();
        t.Wait();
        Console.ReadKey();
    }
}

public class Worker
{
    public async Task<bool> Init()
    {
        var series = Enumerable.Range(1, 5).ToList();
        foreach (var i in series)
        {
            Console.WriteLine("Starting Process {0}", i);
            var result = await DoWorkAsync(i);
            if (result)
            {
                Console.WriteLine("Ending Process {0}", i);
            }
        }

        return true;
    }

    public async Task<bool> DoWorkAsync(int i)
    {
        Console.WriteLine("working..{0}", i);
        await Task.Delay(1000);
        return true;
    }

    public bool ParallelInit()
    {
        var series = Enumerable.Range(1, 5).ToList();
        Parallel.ForEach(series, i =>
        {
            Console.WriteLine("Starting Process {0}", i);
            DoWorkAsync(i);
            Console.WriteLine("Ending Process {0}", i);
        });
        return true;
    }
}
Satish
fonte

Respostas:

123

A maneira como você está usando a awaitpalavra - chave diz ao C # que você deseja esperar cada vez que passar pelo loop, que não é paralelo. Você pode reescrever seu método assim para fazer o que quiser, armazenando uma lista de se, em Taskseguida, awaitcarregando todos com Task.WhenAll.

public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5).ToList();
    var tasks = new List<Task<Tuple<int, bool>>>();
    foreach (var i in series)
    {
        Console.WriteLine("Starting Process {0}", i);
        tasks.Add(DoWorkAsync(i));
    }
    foreach (var task in await Task.WhenAll(tasks))
    {
        if (task.Item2)
        {
            Console.WriteLine("Ending Process {0}", task.Item1);
        }
    }
    return true;
}

public async Task<Tuple<int, bool>> DoWorkAsync(int i)
{
    Console.WriteLine("working..{0}", i);
    await Task.Delay(1000);
    return Tuple.Create(i, true);
}
Tim S.
fonte
3
Não sei sobre outros, mas um paralelo para / foreach parece mais direto para loops paralelos.
Brettski
8
Importante observar que quando você vê a Ending Processnotificação não é quando a tarefa está realmente terminando. Todas essas notificações são despejadas sequencialmente logo após o término da última tarefa. Quando você vir "Finalizando Processo 1", o processo 1 pode ter acabado há muito tempo. Além da escolha de palavras lá, +1.
Asad Saeeduddin de
@Brettski posso estar errado, mas um loop paralelo captura qualquer tipo de resultado assíncrono. Ao retornar um Task <T>, você imediatamente recebe de volta um objeto Task no qual pode gerenciar o trabalho que está sendo executado internamente, como cancelá-lo ou ver exceções. Agora com Async / Await você pode trabalhar com o objeto Task de uma maneira mais amigável - ou seja, você não precisa fazer Task.Result.
The Muffin Man
@Tim S, e se eu quiser retornar um valor com função assíncrona usando o método Tasks.WhenAll?
Mihir de
Seria uma má prática implementar um Semaphorein DoWorkAsyncpara limitar o máximo de tarefas de execução?
C4d
39

Seu código espera que cada operação (usando await) termine antes de iniciar a próxima iteração.
Portanto, você não obtém nenhum paralelismo.

Se você deseja executar uma operação assíncrona existente em paralelo, não é necessário await; você só precisa obter uma coleção de se Taskchamar Task.WhenAll()para retornar uma tarefa que espera por todos eles:

return Task.WhenAll(list.Select(DoWorkAsync));
SLaks
fonte
então você não pode usar nenhum método assíncrono em nenhum loop?
Satish,
4
@Satish: você pode. No entanto, awaitfaz exatamente o oposto do que você deseja - espera que o Tasktermine.
SLaks
Eu queria aceitar sua resposta, mas Tims S tem uma resposta melhor.
Satish
Ou se você não precisa saber quando a tarefa terminou, você pode simplesmente chamar os métodos sem esperar por eles
disklosr
Para confirmar o que essa sintaxe está fazendo - está executando a Tarefa chamada DoWorkAsyncem cada item em list(passando cada item para DoWorkAsync, que suponho que tenha um único parâmetro)?
jbyrd
12
public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5);
    Task.WhenAll(series.Select(i => DoWorkAsync(i)));
    return true;
}
Vladimir
fonte
4

Em C # 7.0 você pode usar nomes semânticos para cada um dos membros da tupla . Aqui está a resposta de Tim S. usando a nova sintaxe:

public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5).ToList();
    var tasks = new List<Task<(int Index, bool IsDone)>>();

    foreach (var i in series)
    {
        Console.WriteLine("Starting Process {0}", i);
        tasks.Add(DoWorkAsync(i));
    }

    foreach (var task in await Task.WhenAll(tasks))
    {
        if (task.IsDone)
        {
            Console.WriteLine("Ending Process {0}", task.Index);
        }
    }

    return true;
}

public async Task<(int Index, bool IsDone)> DoWorkAsync(int i)
{
    Console.WriteLine("working..{0}", i);
    await Task.Delay(1000);
    return (i, true);
}

Você também pode se livrar de task. dentroforeach :

// ...
foreach (var (IsDone, Index) in await Task.WhenAll(tasks))
{
    if (IsDone)
    {
        Console.WriteLine("Ending Process {0}", Index);
    }
}
// ...
Mehdi Dehghani
fonte