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.