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

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

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

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

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);
    }
}
// ...
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.