Ich habe gerade eine merkwürdige Beobachtung bezüglich der Task.WhenAll
Methode gemacht, als ich unter .NET Core 3.0 lief. Ich habe eine einfache Task.Delay
Aufgabe als einzelnes Argument übergeben Task.WhenAll
und erwartet, dass sich die umschlossene Aufgabe identisch mit der ursprünglichen Aufgabe verhält. Dies ist jedoch nicht der Fall. Die Fortsetzungen der ursprünglichen Aufgabe werden asynchron ausgeführt (was wünschenswert ist), und die Fortsetzungen mehrerer Task.WhenAll(task)
Wrapper werden nacheinander synchron ausgeführt (was unerwünscht ist).
Hier ist eine Demo dieses Verhaltens. Vier Worker-Aufgaben warten darauf, dass dieselbe Task.Delay
Aufgabe erledigt wird, und fahren dann mit einer umfangreichen Berechnung fort (simuliert durch a Thread.Sleep
).
var task = Task.Delay(500);
var workers = Enumerable.Range(1, 4).Select(async x =>
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
$" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} before await");
await task;
//await Task.WhenAll(task);
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
$" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} after await");
Thread.Sleep(1000); // Simulate some heavy CPU-bound computation
}).ToArray();
Task.WaitAll(workers);
Hier ist die Ausgabe. Die vier Fortsetzungen werden erwartungsgemäß in verschiedenen Threads (parallel) ausgeführt.
05:23:25.511 [1] Worker1 before await
05:23:25.542 [1] Worker2 before await
05:23:25.543 [1] Worker3 before await
05:23:25.543 [1] Worker4 before await
05:23:25.610 [4] Worker1 after await
05:23:25.610 [7] Worker2 after await
05:23:25.610 [6] Worker3 after await
05:23:25.610 [5] Worker4 after await
Wenn ich nun die Zeile kommentiere await task
und die folgende Zeile auskommentiere await Task.WhenAll(task)
, ist die Ausgabe ganz anders. Alle Fortsetzungen werden im selben Thread ausgeführt, sodass die Berechnungen nicht parallelisiert werden. Jede Berechnung beginnt nach Abschluss der vorherigen:
05:23:46.550 [1] Worker1 before await
05:23:46.575 [1] Worker2 before await
05:23:46.576 [1] Worker3 before await
05:23:46.576 [1] Worker4 before await
05:23:46.645 [4] Worker1 after await
05:23:47.648 [4] Worker2 after await
05:23:48.650 [4] Worker3 after await
05:23:49.651 [4] Worker4 after await
Überraschenderweise geschieht dies nur, wenn jeder Mitarbeiter auf einen anderen Wrapper wartet. Wenn ich den Wrapper im Voraus definiere:
var task = Task.WhenAll(Task.Delay(500));
... und dann await
die gleiche Aufgabe in allen Arbeitern, das Verhalten ist identisch mit dem ersten Fall (asynchrone Fortsetzungen).
Meine Frage ist: Warum passiert das? Was bewirkt, dass die Fortsetzungen verschiedener Wrapper derselben Aufgabe synchron im selben Thread ausgeführt werden?
Hinweis: Das Umschließen einer Aufgabe mit Task.WhenAny
statt Task.WhenAll
führt zu demselben seltsamen Verhalten.
Eine weitere Beobachtung: Ich hatte erwartet, dass das Umschließen des Wrappers in a Task.Run
die Fortsetzungen asynchron machen würde. Aber es passiert nicht. Die Fortsetzungen der folgenden Zeile werden weiterhin im selben Thread ausgeführt (synchron).
await Task.Run(async () => await Task.WhenAll(task));
Erläuterung: Die oben genannten Unterschiede wurden in einer Konsolenanwendung beobachtet, die auf der .NET Core 3.0-Plattform ausgeführt wird. In .NET Framework 4.8 gibt es keinen Unterschied zwischen dem Warten auf die ursprüngliche Aufgabe oder dem Task-Wrapper. In beiden Fällen werden die Fortsetzungen synchron im selben Thread ausgeführt.
quelle
await Task.WhenAll(new[] { task });
?Task.WhenAll
Task.Delay
von100
auf1000
geändert hatte, damit sie beimawait
Bearbeiten nicht abgeschlossen wird .Antworten:
Sie haben also mehrere asynchrone Methoden, die auf dieselbe Taskvariable warten.
Ja, diese Fortsetzungen werden nach Abschluss in Reihe aufgerufen
task
. In Ihrem Beispiel belastet jede Fortsetzung den Thread für die nächste Sekunde.Wenn Sie möchten, dass jede Fortsetzung asynchron ausgeführt wird, benötigen Sie möglicherweise Folgendes:
Damit Ihre Aufgaben von der anfänglichen Fortsetzung zurückkehren und die CPU-Last außerhalb von ausgeführt werden kann
SynchronizationContext
.quelle
Task.Yield
ist eine gute Lösung für mein Problem. Meine Frage ist jedoch mehr darüber, warum dies geschieht, und weniger darüber, wie das gewünschte Verhalten erzwungen werden kann.SynchronizationContext
AufrufConfigureAwait(false)
der ursprünglichen Aufgabe kann ausreichend sein.SynchronizationContext.Current
ist null. Aber ich habe es nur überprüft, um sicherzugehen. Ich fügteConfigureAwait(false)
in derawait
Zeile hinzu und es machte keinen Unterschied. Die Beobachtungen sind die gleichen wie zuvor.Wenn eine Aufgabe mit erstellt wird
Task.Delay()
, werden ihre Erstellungsoptionen aufNone
anstatt gesetztRunContinuationsAsychronously
.Dies könnte den Wechsel zwischen dem .net-Framework und dem .net-Kern unterbrechen. Unabhängig davon scheint es das Verhalten zu erklären, das Sie beobachten. Sie können dies auch aus Graben in den Quellcode überprüfen , die
Task.Delay()
sich Newing auf eine ,DelayPromise
die ruft die Standard -Task
Konstruktor so dass keine Erstellungsoptionen festgelegt.quelle
RunContinuationsAsychronously
zum Standard geworden ist ? Dies würde einige meiner Beobachtungen erklären, aber nicht alle. Insbesondere würde es nicht den Unterschied zwischen dem Warten auf denselben Wrapper und dem Warten auf verschiedene Wrapper erklären .None
Task
Task.WhenAll
In Ihrem Code befindet sich der folgende Code außerhalb des wiederkehrenden Körpers.
Jedes Mal, wenn Sie Folgendes ausführen, wartet es auf die Aufgabe und führt sie in einem separaten Thread aus
Wenn Sie jedoch Folgendes ausführen, wird der Status von überprüft
task
, sodass er in einem Thread ausgeführt wirdWenn Sie jedoch die Aufgabenerstellung daneben verschieben
WhenAll
, wird jede Aufgabe in einem separaten Thread ausgeführt.quelle
Task.WhenAll
ist nur eine reguläreTask
, wie das Originaltask
. Beide Aufgaben werden irgendwann abgeschlossen, das Original als Ergebnis eines Timer-Ereignisses und das Composite als Ergebnis des Abschlusses der ursprünglichen Aufgabe. Warum sollten ihre Fortsetzungen ein anderes Verhalten zeigen? In welchem Aspekt unterscheidet sich die eine Aufgabe von der anderen?