Jak korzystać z await in a loop

86

Próbuję utworzyć asynchroniczną aplikację konsolową, która wykonuje trochę pracy nad kolekcją. Mam jedną wersję, która używa równoległej pętli dla innej wersji, która używa async / await. Spodziewałem się, że wersja async / await będzie działać podobnie do wersji równoległej, ale jest wykonywana synchronicznie. Co ja robię źle?

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
źródło

Odpowiedzi:

124

Sposób, w jaki używasz awaitsłowa kluczowego, mówi C #, że chcesz czekać za każdym razem, gdy przechodzisz przez pętlę, co nie jest równoległe. Możesz przepisać swoją metodę w ten sposób, aby robić to, co chcesz, przechowując listę Taski następnie awaitje wszystkie za pomocą 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.
źródło
3
Nie wiem o innych, ale paralela for / foreach wydaje się prostsza w przypadku pętli równoległych.
Brettski
8
Należy pamiętać, że gdy zobaczysz Ending Processpowiadomienie, nie oznacza to , że zadanie się kończy. Wszystkie te powiadomienia są wyrzucane sekwencyjnie zaraz po zakończeniu ostatniego zadania. Zanim zobaczysz komunikat „Zakończenie procesu 1”, proces 1 mógł już dawno się skończyć. Poza wyborem słów, +1.
Asad Saeeduddin
@Brettski Mogę się mylić, ale równoległa pętla przechwytuje dowolny wynik asynchroniczny. Zwracając Task <T>, natychmiast odzyskujesz obiekt Task, w którym możesz zarządzać pracą, która odbywa się w środku, na przykład anulowanie jej lub wyświetlanie wyjątków. Teraz dzięki Async / Await możesz pracować z obiektem Task w bardziej przyjazny sposób - to znaczy nie musisz wykonywać Task.Result.
The Muffin Man
@Tim S, co jeśli chcę zwrócić wartość z funkcją asynchroniczną przy użyciu metody Tasks.WhenAll?
Mihir
Czy byłoby złą praktyką wdrożenie Semaphorein w DoWorkAsynccelu ograniczenia maksymalnej liczby wykonywanych zadań?
C4d
39

Twój kod czeka na zakończenie każdej operacji (użycie await) przed rozpoczęciem następnej iteracji.
Dlatego nie otrzymujesz żadnej równoległości.

Jeśli chcesz uruchomić istniejącą operację asynchroniczną równolegle, nie potrzebujesz await; musisz tylko pobrać kolekcję Tasksi wywołać, Task.WhenAll()aby zwrócić zadanie, które czeka na wszystkie z nich:

return Task.WhenAll(list.Select(DoWorkAsync));
SLaks
źródło
więc nie możesz używać żadnych metod asynchronicznych w żadnej pętli?
Satish
4
@ Satish: Możesz. Jednak awaitrobi dokładnie odwrotność tego, co chcesz - czeka na Taskzakończenie.
SLaks
Chciałem przyjąć twoją odpowiedź, ale Tims S ma lepszą odpowiedź.
Satish
Lub jeśli nie musisz wiedzieć, kiedy zadanie się zakończyło, możesz po prostu wywołać metody bez
czekania
Aby potwierdzić, co robi ta składnia - uruchamia zadanie wywołane DoWorkAsyncna każdym elemencie list(przekazując każdy element do DoWorkAsync, który zakładam, że ma jeden parametr)?
jbyrd
12
public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5);
    Task.WhenAll(series.Select(i => DoWorkAsync(i)));
    return true;
}
Vladimir
źródło
4

W C # 7.0 możesz użyć nazw semantycznych dla każdego z członków krotki , oto odpowiedź Tima S. przy użyciu nowej składni:

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

Możesz też pozbyć się task. wnętrzaforeach :

// ...
foreach (var (IsDone, Index) in await Task.WhenAll(tasks))
{
    if (IsDone)
    {
        Console.WriteLine("Ending Process {0}", Index);
    }
}
// ...
Mehdi Dehghani
źródło