Come usare await in un loop


87

Sto cercando di creare un'app per console asincrona che esegue un po 'di lavoro su una raccolta. Ho una versione che usa parallel for loop un'altra versione che usa async / await. Mi aspettavo che la versione async / await funzionasse in modo simile alla versione parallela, ma viene eseguita in modo sincrono. Che cosa sto facendo di sbagliato?

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

Risposte:


126

Il modo in cui stai usando la awaitparola chiave dice a C # che vuoi aspettare ogni volta che passi attraverso il ciclo, che non è parallelo. Puoi riscrivere il tuo metodo in questo modo per fare quello che vuoi, memorizzando un elenco di se Taskpoi awaitinserirli tutti 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);
}

3
Non so degli altri, ma un parallelo per / foreach sembra più diretto per i cicli paralleli.
Brettski

8
È importante notare che quando vedi la Ending Processnotifica non è quando l'attività sta effettivamente terminando. Tutte queste notifiche vengono scaricate in sequenza subito dopo il termine dell'ultima attività. Quando vedi "Ending Process 1", il processo 1 potrebbe essere terminato da molto tempo. Oltre alla scelta delle parole, +1.
Asad Saeeduddin

@Brettski Potrei sbagliarmi, ma un ciclo parallelo intrappola qualsiasi tipo di risultato asincrono. Restituendo un Task <T> si ottiene immediatamente un oggetto Task in cui è possibile gestire il lavoro che si sta svolgendo all'interno come annullarlo o vedere eccezioni. Ora con Async / Await puoi lavorare con l'oggetto Task in modo più amichevole, ovvero non devi eseguire Task.Result.
The Muffin Man

@Tim S, cosa succede se voglio restituire un valore con la funzione asincrona utilizzando il metodo Tasks.WhenAll?
Mihir

Sarebbe una cattiva pratica implementare un Semaphorein DoWorkAsyncper limitare il numero massimo di attività di esecuzione?
C4d

39

Il codice attende che ogni operazione (utilizzando await) finisca prima di iniziare l'iterazione successiva.
Pertanto, non ottieni alcun parallelismo.

Se si desidera eseguire un'operazione asincrona esistente in parallelo, non è necessario await; devi solo ottenere una raccolta di se Taskchiamare Task.WhenAll()per restituire un'attività che li attende tutti:

return Task.WhenAll(list.Select(DoWorkAsync));

quindi non puoi usare metodi asincroni in nessun ciclo?
Satish

4
@ Satish: Puoi. Tuttavia, awaitfa l'esatto opposto di ciò che si vuole - si attende per il Taskper finire.
SLaks

Volevo accettare la tua risposta, ma Tims S ha una risposta migliore.
Satish

Oppure, se non hai bisogno di sapere quando l'attività è terminata, puoi semplicemente chiamare i metodi senza aspettarli
disklosr

Per confermare cosa sta facendo quella sintassi - sta eseguendo l'attività chiamata DoWorkAsyncsu ogni elemento in list(passando ogni elemento in DoWorkAsync, che presumo abbia un singolo parametro)?
jbyrd

12
public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5);
    Task.WhenAll(series.Select(i => DoWorkAsync(i)));
    return true;
}

4

In C # 7.0 è possibile utilizzare nomi semantici per ciascuno dei membri della tupla , ecco la risposta di Tim S. utilizzando la nuova sintassi:

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

Potresti anche sbarazzarti di task. dentroforeach :

// ...
foreach (var (IsDone, Index) in await Task.WhenAll(tasks))
{
    if (IsDone)
    {
        Console.WriteLine("Ending Process {0}", Index);
    }
}
// ...
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.