Verwendung von await in einer Schleife

85

Ich versuche, eine asynchrone Konsolen-App zu erstellen, die einige Arbeiten an einer Sammlung ausführt. Ich habe eine Version, die parallel for loop verwendet, eine andere Version, die async / await verwendet. Ich habe erwartet, dass die asynchrone / warte-Version ähnlich wie die parallele Version funktioniert, aber sie wird synchron ausgeführt. Was mache ich falsch?

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
quelle

Antworten:

121

Die Art und Weise, wie Sie das awaitSchlüsselwort verwenden, teilt C # mit, dass Sie jedes Mal warten möchten, wenn Sie die nicht parallele Schleife durchlaufen. Sie können Ihre Methode so umschreiben, um das zu tun, was Sie wollen, indem Sie eine Liste von Tasks speichern und sie dann awaitalle mit bearbeiten 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.
quelle
3
Ich weiß nichts über andere, aber eine Parallele für / foreach scheint für Parallelschleifen einfacher zu sein.
Brettski
8
Es ist wichtig zu beachten, dass die Ending ProcessBenachrichtigung nicht angezeigt wird, wenn die Aufgabe tatsächlich beendet wird. Alle diese Benachrichtigungen werden direkt nach Abschluss der letzten Aufgabe nacheinander ausgegeben. Zu dem Zeitpunkt, an dem "Prozess 1 beenden" angezeigt wird, ist Prozess 1 möglicherweise schon lange beendet. Abgesehen von der Wortwahl dort +1.
Asad Saeeduddin
@Brettski Ich kann mich irren, aber eine parallele Schleife fängt jede Art von asynchronem Ergebnis ab. Wenn Sie eine Aufgabe <T> zurückgeben, erhalten Sie sofort ein Aufgabenobjekt zurück, in dem Sie die darin laufenden Arbeiten verwalten können, z. B. das Abbrechen oder das Anzeigen von Ausnahmen. Mit Async / Await können Sie jetzt freundlicher mit dem Task-Objekt arbeiten - das heißt, Sie müssen Task.Result nicht ausführen.
Der Muffin-Mann
@ Tim S, was ist, wenn ich mit der Tasks.WhenAll-Methode einen Wert mit asynchroner Funktion zurückgeben möchte?
Mihir
Wäre es eine schlechte Praxis, ein SemaphoreIn DoWorkAsynczu implementieren , um die maximal ausgeführten Aufgaben zu begrenzen?
C4d
38

Ihr Code wartet, bis jede Operation (mit await) abgeschlossen ist, bevor die nächste Iteration gestartet wird.
Daher erhalten Sie keine Parallelität.

Wenn Sie eine vorhandene asynchrone Operation parallel ausführen möchten, benötigen Sie keine await; Sie müssen nur eine Sammlung von Tasks abrufen und aufrufen Task.WhenAll(), um eine Aufgabe zurückzugeben, die auf alle wartet:

return Task.WhenAll(list.Select(DoWorkAsync));
SLaks
quelle
Sie können also keine asynchronen Methoden in Schleifen verwenden?
Satish
4
@ Satish: Das kannst du. Tut awaitgenau das Gegenteil von dem, was Sie wollen - es wartet darauf, dass das TaskEnde ist.
SLaks
Ich wollte Ihre Antwort akzeptieren, aber Tims S hat eine bessere Antwort.
Satish
Oder wenn Sie nicht wissen müssen, wann die Aufgabe beendet ist, können Sie die Methoden einfach aufrufen, ohne darauf zu warten
disklosr
Um zu bestätigen, was diese Syntax tut, wird die DoWorkAsyncfür jedes Element aufgerufene Aufgabe ausgeführt list(Übergabe jedes Elements an DoWorkAsync, von dem ich annehme, dass es einen einzelnen Parameter hat)?
4.
12
public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5);
    Task.WhenAll(series.Select(i => DoWorkAsync(i)));
    return true;
}
Vladimir
quelle
4

In C # 7.0 können Sie jedem Mitglied des Tupels semantische Namen geben. Hier ist die Antwort von Tim S. mit der neuen Syntax:

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

Sie könnten auch task. innenforeach loswerden :

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