Cómo usar await in a loop

86

Estoy tratando de crear una aplicación de consola asincrónica que funcione un poco en una colección. Tengo una versión que usa paralelo para bucle, otra versión que usa async / await. Esperaba que la versión async / await funcionara de manera similar a la versión paralela, pero se ejecuta sincrónicamente. ¿Qué estoy haciendo mal?

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
fuente

Respuestas:

124

La forma en que está usando la awaitpalabra clave le dice a C # que desea esperar cada vez que pasa por el ciclo, que no es paralelo. Puede reescribir su método de esta manera para hacer lo que quiera, almacenando una lista de Tasksy luego awaitingrándolos todos con 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.
fuente
3
No sé sobre otros, pero un for / foreach paralelo parece más sencillo para los bucles paralelos.
Brettski
8
Es importante tener en cuenta que cuando ve la Ending Processnotificación no es cuando la tarea realmente está finalizando. Todas esas notificaciones se descargan secuencialmente justo después de que finaliza la última de las tareas. Para cuando vea "Finalizar proceso 1", es posible que el proceso 1 haya terminado durante mucho tiempo. Aparte de la elección de palabras allí, +1.
Asad Saeeduddin
@Brettski Puedo estar equivocado, pero un bucle paralelo atrapa cualquier tipo de resultado asincrónico. Al devolver una Task <T>, inmediatamente recupera un objeto Task en el que puede administrar el trabajo que se está realizando en el interior, como cancelarlo o ver excepciones. Ahora, con Async / Await puede trabajar con el objeto Task de una manera más amigable, es decir, no tiene que hacer Task.Result.
The Muffin Man
@Tim S, ¿qué pasa si quiero devolver un valor con la función asíncrona usando el método Tasks.WhenAll?
Mihir
¿Sería una mala práctica implementar un Semaphorein DoWorkAsyncpara limitar el máximo de tareas de ejecución?
C4d
39

Su código espera awaita que finalice cada operación (uso ) antes de comenzar la siguiente iteración.
Por tanto, no se obtiene ningún paralelismo.

Si desea ejecutar una operación asincrónica existente en paralelo, no es necesario await; solo necesita obtener una colección de Tasksy llamar Task.WhenAll()para devolver una tarea que espera a todos ellos:

return Task.WhenAll(list.Select(DoWorkAsync));
SLaks
fuente
¿Entonces no puede usar ningún método asincrónico en ningún bucle?
Sábado
4
@Satish: Puedes. Sin embargo, awaithace exactamente lo contrario de lo que desea: espera a Taskque termine.
SLaks
Quería aceptar tu respuesta, pero Tims S tiene una mejor respuesta.
Sábado
O si no necesita saber cuándo terminó la tarea, puede simplemente llamar a los métodos sin
esperarlos
Para confirmar lo que está haciendo esa sintaxis, ¿está ejecutando la Tarea llamada DoWorkAsyncen cada elemento list(pasando cada elemento DoWorkAsync, que supongo que tiene un solo 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
fuente
4

En C # 7.0 puede usar nombres semánticos para cada uno de los miembros de la tupla , aquí está la respuesta de Tim S. usando la nueva sintaxis:

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);
}

También puedes deshacerte del task. interiorforeach :

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