Nachdem ich mich eine Weile mit dem asynchronen / wartenden Muster von C # befasst hatte, wurde mir plötzlich klar, dass ich nicht wirklich weiß, wie ich erklären soll, was im folgenden Code passiert:
async void MyThread()
{
while (!_quit)
{
await GetWorkAsync();
}
}
GetWorkAsync()
Es wird angenommen, dass ein Wartender zurückgegeben wird, Task
der möglicherweise einen Threadwechsel verursacht oder nicht, wenn die Fortsetzung ausgeführt wird.
Ich wäre nicht verwirrt, wenn das Warten nicht in einer Schleife wäre. Ich würde natürlich erwarten, dass der Rest der Methode (dh die Fortsetzung) möglicherweise auf einem anderen Thread ausgeführt wird, was in Ordnung ist.
Innerhalb einer Schleife wird das Konzept des "Restes der Methode" für mich jedoch etwas neblig.
Was passiert mit "dem Rest der Schleife", wenn der Thread auf Fortsetzung geschaltet ist oder wenn er nicht geschaltet ist? Auf welchem Thread wird die nächste Iteration der Schleife ausgeführt?
Meine Beobachtungen zeigen (nicht endgültig verifiziert), dass jede Iteration auf demselben Thread (dem ursprünglichen) beginnt, während die Fortsetzung auf einem anderen ausgeführt wird. Kann das wirklich sein? Wenn ja, ist dies dann ein Grad unerwarteter Parallelität, der im Hinblick auf die Thread-Sicherheit der GetWorkAsync-Methode berücksichtigt werden muss?
UPDATE: Meine Frage ist kein Duplikat, wie von einigen vorgeschlagen. Das while (!_quit) { ... }
Codemuster ist lediglich eine Vereinfachung meines tatsächlichen Codes. In Wirklichkeit ist mein Thread eine langlebige Schleife, die ihre Eingabewarteschlange mit Arbeitselementen in regelmäßigen Abständen (standardmäßig alle 5 Sekunden) verarbeitet. Die tatsächliche Beendigungsbedingungsprüfung ist auch keine einfache Feldprüfung, wie im Beispielcode vorgeschlagen, sondern eine Ereignishandle-Prüfung.
Antworten:
Sie können es tatsächlich bei Try Roslyn ausprobieren . Ihre Wartemethode wird in
void IAsyncStateMachine.MoveNext()
die generierte asynchrone Klasse umgeschrieben .Was Sie sehen werden, ist ungefähr so:
Grundsätzlich spielt es keine Rolle, auf welchem Thread Sie sich befinden. Die Zustandsmaschine kann ordnungsgemäß fortgesetzt werden, indem Ihre Schleife durch eine äquivalente if / goto-Struktur ersetzt wird.
Allerdings werden asynchrone Methoden nicht unbedingt auf einem anderen Thread ausgeführt. In Eric Lipperts Erklärung "Es ist keine Magie" wird erklärt, wie Sie
async/await
nur an einem Thread arbeiten können.quelle
Erstens hat Servy einen Code in eine Antwort auf eine ähnliche Frage geschrieben, auf der diese Antwort basiert:
/programming/22049339/how-to-create-a-cancellable-task-loop
Servys Antwort enthält eine ähnliche
ContinueWith()
Schleife mit TPL-Konstrukten ohne explizite Verwendung der Schlüsselwörterasync
undawait
. Um Ihre Frage zu beantworten, überlegen Sie, wie Ihr Code aussehen könnte, wenn Ihre Schleife mit abgewickelt wirdContinueWith()
Es dauert einige Zeit, bis Sie Ihren Kopf umwickelt haben, aber zusammenfassend:
continuation
stellt einen Abschluss für die "aktuelle Iteration" darprevious
stellt denTask
Status der "vorherigen Iteration" dar (dh es weiß, wann die "Iteration" beendet ist und wird zum Starten der nächsten verwendet.)GetWorkAsync()
a zurückgegebenTask
wird, bedeutet dies, dass a zurückgegebenContinueWith(_ => GetWorkAsync())
wird,Task<Task>
daher der AufrufUnwrap()
, die 'innere Aufgabe' zu erhalten (dh das tatsächliche Ergebnis vonGetWorkAsync()
).Damit:
Task.FromResult(_quit)
- sein Status beginnt alsTask.Completed == true
.continuation
wird zum ersten Mal mit ausgeführtprevious.ContinueWith(continuation)
continuation
Abschluss wird aktualisiertprevious
, um den Abschlussstatus von widerzuspiegeln_ => GetWorkAsync()
_ => GetWorkAsync()
es abgeschlossen ist, wird es "fortgesetzt mit"_previous.ContinueWith(continuation)
- dh dascontinuation
Lambda erneut aufrufenprevious
der Status aktualisiert,_ => GetWorkAsync()
sodass dascontinuation
Lambda bei derGetWorkAsync()
Rückkehr aufgerufen wird .Das
continuation
Lambda prüft immer den Zustand von_quit
, wenn_quit == false
dann keine Fortsetzungen mehr vorhanden sind und dasTaskCompletionSource
auf den Wert von gesetzt_quit
wird und alles abgeschlossen ist.Was Ihre Beobachtung bezüglich der Fortsetzung betrifft, die in einem anderen Thread ausgeführt wird, ist dies nicht das, was das Schlüsselwort
async
/await
für Sie tun würde, wie in diesem Blog "Aufgaben sind (noch) keine Threads und Async ist nicht parallel" . - https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/Ich würde vorschlagen, dass es sich in der Tat lohnt, Ihre
GetWorkAsync()
Methode in Bezug auf Gewinde und Gewindesicherheit genauer zu betrachten. Wenn Ihre Diagnose ergibt, dass es infolge Ihres wiederholten asynchronen / wartenden Codes auf einem anderen Thread ausgeführt wurde, muss etwas innerhalb oder im Zusammenhang mit dieser Methode dazu führen, dass an anderer Stelle ein neuer Thread erstellt wird. (Wenn dies unerwartet ist, gibt es vielleicht.ConfigureAwait
irgendwo einen?)quelle
SynchronizationContext
- das ist sicherlich wichtig, da.ContinueWith()
der SynchronizationContext zum Versenden der Fortsetzung verwendet wird; Es würde in der Tat das Verhalten erklären, das Sie sehen, wennawait
es in einem ThreadPool-Thread oder einem ASP.NET-Thread aufgerufen wird. Eine Fortsetzung könnte in diesen Fällen sicherlich an einen anderen Thread gesendet werden. Auf der anderen Seite sollte das Aufrufenawait
eines Single-Threaded-Kontexts wie eines WPF-Dispatchers oder eines Winforms-Kontexts ausreichen, um sicherzustellen, dass die Fortsetzung auf dem Original erfolgt. Thread