Laden...

asynchrone Programmierung: Task vs. Threads, async vs. parallel, in welchem Thread läuft was?

Erstellt von Paschulke vor 9 Jahren Letzter Beitrag vor 9 Jahren 6.004 Views
Hinweis von herbivore vor 9 Jahren

Der ursprüngliche Titel war: "Grundlegende Fragen zu asynchroner Programmierung"

P
Paschulke Themenstarter:in
69 Beiträge seit 2011
vor 9 Jahren
asynchrone Programmierung: Task vs. Threads, async vs. parallel, in welchem Thread läuft was?

Hallo,

die asynchrone Programmierung bereitet mir Probleme. Irgendwie läuft mein (WPF) Programm, dann rausche ich aber immer wieder in Probleme mit der Aktualisierung der GUI, da ich mich plötzlich mal wieder nicht im STA Thread befinde.
Ich fürchte, dass ich auf einem gefährlichen Halbwissen entwickle...

Meine erste Frage:
Wenn ich mit Hilfe von Task.Factory.StartNew einen neuen asynchronen Prozess starte, wird dann immer ein neuer Thread erzeugt? Irgendein "Experte" hatte mir mal erklärt, dass asynchron nicht gleich multithreaded ist und es wichtig ist, das zu verstehen. Offensichtlich habe ich es nicht verstanden. Kann mir das nochmal jemand erklären - sofern die Aussage stimmt?

Zweitens:
Wenn ich mit await auf die Fertigstellung eines asynchronen Prozesses warte, kehre ich dann immer in den aufrufenden Thread zurück? Davon bin ich bisher ausgegangen.

Drittens (erledigt sich evtl. mit Frage 1):
Besteht ein Unterschied zwischen einem asynchronen Prozess aus dem .NET-Framework (z.B. Stream.ReadAsync) und einem von mir erzeugten Prozess mit Task.Factory.StartNew?

Viertens:
Ich entwickle eine Client-Server-Application mit WPF und WCF. Die Erzeugung der Services übernimmt der IoC-Container Castle.Windsor. Um die Services asynchron aufzurufen nutze ich eine im folgenden Thread beschriebene Erweiterungsmethode: Stackoverflow (1. Antwort): Using Client-Side Task-Based Operations with WCFFacility in Castle.Windsor. Ist das sauber?

So das sollten erst einmal genug Fragen sein...

W
955 Beiträge seit 2010
vor 9 Jahren

Hallo,

ich mache mal ein Anfang.
zu 1.
Du könntest mit dem Debugger mal rumsteppen und schauen wo Kontextwechsel stattfinden
zu 2.
Ja, nach dem await wird mit dem Thread weitergearbeitet der vor dem await gearbeitet hat. Wenn Du das nicht willst (also weiter unten im Service/Repository-Bereich) kannst Du das mit await func().ConfigureAwait(false) angeben weil es dort egal ist mit welchem ThreadpoolThread gearbeitet wird. In der Application-Ebene nimmt man das eher nicht damit man die GUI aktualisieren kann.
Ich würde bei 3. und 4. allgemein empfehlen sich entsprechende Blogs im Netz durchzulesen, vor allem von Stephen Toub und Stephen Cleary. Dort gibt es viele nützliche Tipps und man versteht das Konzept dahinter besser.

16.828 Beiträge seit 2008
vor 9 Jahren

Wenn ich mit Hilfe von Task.Factory.StartNew einen neuen asynchronen Prozess starte, wird dann immer ein neuer Thread erzeugt?

Nein. Es kann sein, dass ein Task ein Thread hat. Es kann aber auch sein, dass sich mehrere Tasks einen Thread teilen.
Kommt auf die aktuelle Auslastung und das Scheduling an.

Contextwechsel kannst Du explzit verhindern (empfohlen), erzwingen oder auf "Auto" (Default) lassen.
Siehe dazu Bewährte Verfahren bei der asynchronen Programmierung

Irgendein "Experte" hatte mir mal erklärt, dass asynchron nicht gleich multithreaded ist und es wichtig ist, das zu verstehen.

Prinzipiell ist await/async eine Art der Multi-Threadeed-Programmierung.
Multi-Threaded heisst nicht unbedingt, dass auch wirklich mehrere Threads verwendet werden.
Es reicht hier schon die Kapslung in Tasks, denn das Erstellen und Verwalten von Tasks ist viel "günstiger" als teure Threads.
Ob eine Anwendung nun mehrere Tasks oder mehrere Threads zur Abbilund von Parallelität verwenden ist zu 99,999% unerheblich und es wird kein Unterschied (in der .NET Wet) gespürt.
Anders mag es in der Programmierung aussehen: mit Tasks zu arbeiten ist deutlich einfacher.

Wenn ich mit await auf die Fertigstellung eines asynchronen Prozesses warte, kehre ich dann immer in den aufrufenden Thread zurück?

Ein "Warten" gibt es prinzipiell mit await/async nicht. Dahinter steckt sehr viel Magic des Compilers, der Callbacks um die betreffenden Stellen baut.

Aber


MyClass item = await myRepository.GetAnyAsync();
var name = item.Name;

Hier würde bei name solange "gewartet" werden, bis das Async() fertig ist. Erst dann wird name gesetzt.

Besteht ein Unterschied zwischen einem asynchronen Prozess aus dem .NET-Framework (z.B. Stream.ReadAsync) und einem von mir erzeugten Prozess mit Task.Factory.StartNew?

Ja. Bei async/await bzw. .NET Async()-Methoden sind die Tasks so umgesetzt, dass das Exceptionhandling bereits korrekt umgewandelt bzw. verarbeitet wird. Das ist auch ein absolutes Muss für eigene Async() Methoden.
Wer sich dafür interessiert, was dahinter steckt, der kann sich mein NuGet-Paket "AsyncAll anschauen (arg viel anders bzw. teilweise identisch macht es .NET selbst).
Der Quellcode ist dazu weitgehen in meinem anderen Projekt QuickIO sichtbar:AsyncExtensions

Zu viertens kann ich nichts sagen; arbeite nicht mit Windsor.

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo Paschulke,

vorneweg: bitte beachte beim nächsten Mal [Hinweis] Wie poste ich richtig? Punkt 1.2 und 3.

Irgendein "Experte" hatte mir mal erklärt, dass asynchron nicht gleich multithreaded ist

Richtig, siehe Erstellen einer entsprechenden sync Funktion für eine async Funktion (oder besser andersherum?) ff.

Wenn ich mit await auf die Fertigstellung eines asynchronen Prozesses warte, kehre ich dann immer in den aufrufenden Thread zurück?

Nein, ein Thread tut immer das, was seine Programmierung (z.B. ThreadStart-Methode) vorgibt. Wenn daraus eine asynchrone Methode aufgerufen wird, läuft die ThreadStart-Methode weiter und tut, was immer durch ihre Programmierung vorgegeben ist. Es gibt keine Möglichkeit, dass ein anderer Thread die auszuführenden Befehle bestimmt. Nur wenn ein Thread explizit kooperativ programmiert ist, so dass er Nachrichten von anderen Threads entgegen nimmt und darauf reagiert, besteht die Möglichkeit, dass die Ausführung bestimmter Aktionen an den Thread zurückdelegiert wird. Beim GUI-Thread ist das der Fall und das Zurückdelegierten erfolgt auch standardmäßig. Es kann aber explizit verhindert werden, siehe dazu auch [FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke) (u.a. "Verhindern, dass die Continuation im GUI-Thread ausgeführt wird"). Bei einem Aufruf eine asynchronen Methode aus einem Consolenprogramm (z.B. aus dem Main) besteht keine Möglichkeit des Zurückdelegierens, weshalb die Continuation im gleichen Thread ausgeführt wird, wie die asynchrone Operation, oder sogar noch eher in einem weiteren (dritten) Thread.

Siehe auch Eleganteste Art aus Worker-Thread auf Controls zugreifen [generell Kontrollfluss zwischen Threads].

herbivore

P
Paschulke Themenstarter:in
69 Beiträge seit 2011
vor 9 Jahren

Vielen Dank für die interessanten Antworten!
Ich werde sie mir in Ruhe ansehen. Ich denke sie bringen mich ein großes Stück weiter 😃

6.911 Beiträge seit 2009
vor 9 Jahren

Hallo Abt,

Aber

  
MyClass item = await myRepository.GetAnyAsync();  
var name = item.Name;  
  

Hier würde bei name solange "gewartet" werden, bis das Async() fertig ist. Erst dann wird name gesetzt.

Bei name wird nicht "gewartet". Die asynchrone Fortführung (das "Warten") findet durch Zuweisung des Ergebnisses von GetAnyAsync an item statt.
D.h. sobald die asynchrone Ausführung von GetAnyAsync ein Ergebnis parat hat, so wird das duch Compiler-Magic erstellte Callback ausgeführt und der auf await folgende Code ausgeführt -hier also die Zuweisung an item.

Hallo Paschulke,

auch wenn du auf deine Fragen schon Antworten erhalten hast, will ich dennoch etwas schreiben, da es gerade bei solchen Themen und gutes Verständnis geht und da können mehrere Betrachtungsweise eher zum Verstehen führen.

Wenn ich mit Hilfe von Task.Factory.StartNew einen neuen asynchronen Prozess starte, wird dann immer ein neuer Thread erzeugt?

Mit Task.Factory.StartNew wird ein Task-Objekt erzeugt und diesem dem Task-Scheduler übergeben, der die Ausführung des Task "plant". In .net gibt es mehrere Task-Scheduler, der standardmäßige ist der ThreadPool-Scheduler. D.h. der Task wird als "Arbeitsaufgabe" an den ThreadPool übergeben und wird dort in einem Thread aus dem ThreadPool ausgeführt. Konkret zu deiner Frage heißt das, dass mit den Standardeinstellungen kein neuer Thread gestartet wird.

Es gibt deshalb mehrere verschiedene Task-Scheduler, da der ThreadPool-Scheduler nicht für jede Aufgabe die ideale Wahl ist. Für nicht allzulang* dauernde CPU-lastige Aufgaben ist dieser jedoch meist die beste Wahl.

* lang ist ein sehr unpräziser Begriff. Wie lange "lang" tatsächlich ist, hängt von der Anwendung ab und kann genau nur durch Probieren und Profilieren einer Anwendung ermittelt werden. Als groben Richtwert kannst du aber 1s nehmen.

Wenn ich mit await auf die Fertigstellung eines asynchronen Prozesses warte, kehre ich dann immer in den aufrufenden Thread zurück?

Im Standardverhalten erstellt der Compiler einen Code für await, der den aktuellen SynchronizationContext "einfängt" und die Fortfürhung des Codes ggf. per SynchronizationContext.Post durchführt. Ist kein SynchronizationContext vorhanden, so wird bei Vorhandensein eines Task-Scheduler die Fortführung (die Continuation) an den Scheduler übergeben.

Besteht ein Unterschied zwischen einem asynchronen Prozess aus dem .NET-Framework (z.B. Stream.ReadAsync) und einem von mir erzeugten Prozess mit Task.Factory.StartNew?

In .net gibt es grundsätzlich 2 Arten von ThreadPools. Den CPU-ThreadPool und den IO-ThreadPool.
Ersterer kann und wird üblicherweise von Benutzercode angesprochen, so auch durch den ThreadPool-Taskscheduler der bei Task.Factory.StartNew angewandt wird.
Bei den asynchronen Vorgängen aus .net wie z.B. Stream.ReadAsync wird diese Aufgabe asynchron erledigt und am Ende, sozusagen zum Abschluss, wird ein Thread aus dem IO-ThreadPool (ein "Abschlussthread") verwendet.

Es ist so gut wie immer besser die asynchronen Vorgänge von .net zu verwenden, falls welche existieren.
Nimm als Beispiel den (asynchronen) Download einer Datei aus dem Internet her. Durch DownloadAsync blockiert weder eine GUI, noch wird ein CPU-Thread verbraten während nur gewartet wird und die CPU-Thread können für andere Aufgaben verwendet werden - dies ist v.a. bei Server-Anwendungen sinnvoll, da so z.B. mehr Request gehandelt werden können.
Wenn der Download fertig ist, so wird ein IO-Thread verwendet und führt den (kurzen) Rest der Methode nach dem Aufruf zum Download aus.

Siehe auch Async/Await FAQ

Ich entwickle eine Client-Server-Application mit WPF und WCF

Die Erweiterungsmethode ist laut

Zitat von: wcf - Using Client-Side Task-Based Operations with WCFFacility in Castle.Windso
The WCFFacility provides some extension methods that conform to the old async pattern.

nur für das "klassische" Async-Pattern in WCF nötig. Seit .net 4.5 ist dies jedoch obsolet und es können im ServiceContract auch Task<T> verwendet werden. Somit ist diese Erweiterungsmethode nicht nötig. Castle.Windsor verwendet ich auch nicht, daher kann ich nicht beurteilen ob es in diesem Zusammenhang notwendig ist od. nicht, aber gestützt auf obiges Zitat dürfte es nicht der Fall sein.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

16.828 Beiträge seit 2008
vor 9 Jahren

D.h. sobald die asynchrone Ausführung von GetAnyAsync ein Ergebnis parat hat, so wird das duch Compiler-Magic erstellte Callback ausgeführt und der auf await folgende Code ausgeführt -hier also die Zuweisung an item.

Ein "Warten" gibt es prinzipiell mit await/async nicht. Dahinter steckt sehr viel Magic des Compilers, der Callbacks um die betreffenden Stellen baut.

Wieso hab ich "warten" wohl in Anführungszeichen gestellt....

6.911 Beiträge seit 2009
vor 9 Jahren

Hallo Abt,

Wieso hab ich "warten" wohl in Anführungszeichen gestellt....

Naja, ich bin mir sicher, dass du weißt dass nicht wirklich - also blockierend - gewartet wird.
Dennoch ist

Hier würde bei name solange "gewartet" werden, bis das Async() fertig ist. Erst dann wird name gesetzt.

nicht korrekt. Die Anführungszeichen ändern daran auch nichts 😉
Korrekt ist/wäre: Hier würde bei item solange "gewartet" werden, bis Async() fertig ist.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"