Der asynchrone Ansatz von Microsoft ist ein guter Ersatz für die häufigsten Zwecke der Multithread-Programmierung: Verbesserung der Reaktionsfähigkeit bei E / A-Aufgaben.
Es ist jedoch wichtig zu wissen, dass der asynchrone Ansatz nicht in der Lage ist, die Leistung oder die Reaktionsfähigkeit bei CPU-intensiven Aufgaben zu verbessern.
Multithreading für Reaktionsfähigkeit
Multithreading für die Reaktionsfähigkeit ist die traditionelle Methode, um ein Programm während schwerer E / A-Aufgaben oder schwerer Rechenaufgaben reaktionsfähig zu halten. Sie speichern Dateien in einem Hintergrund-Thread, damit der Benutzer seine Arbeit fortsetzen kann, ohne warten zu müssen, bis die Festplatte ihre Aufgabe abgeschlossen hat. Der E / A-Thread blockiert häufig das Warten, bis ein Teil des Schreibvorgangs abgeschlossen ist, sodass Kontextwechsel häufig vorkommen.
In ähnlicher Weise möchten Sie bei einer komplexen Berechnung eine regelmäßige Kontextumschaltung zulassen, damit die Benutzeroberfläche weiterhin reagiert und der Benutzer denkt, dass das Programm nicht abgestürzt ist.
Das Ziel ist hier im Allgemeinen nicht, dass die mehreren Threads auf verschiedenen CPUs ausgeführt werden. Stattdessen sind wir nur daran interessiert, Kontextwechsel zwischen der lang laufenden Hintergrundaufgabe und der Benutzeroberfläche zu veranlassen, damit die Benutzeroberfläche aktualisiert werden und dem Benutzer antworten kann, während die Hintergrundaufgabe ausgeführt wird. Im Allgemeinen nimmt die Benutzeroberfläche nicht viel CPU-Leistung in Anspruch, und das Threading-Framework oder das Betriebssystem entscheiden sich normalerweise dafür, sie auf derselben CPU auszuführen.
Wir verlieren tatsächlich die Gesamtleistung aufgrund der zusätzlichen Kosten für die Kontextumschaltung, aber das ist uns egal, da die Leistung der CPU nicht unser Ziel war. Wir wissen, dass wir in der Regel mehr CPU-Leistung haben, als wir benötigen, und daher besteht unser Ziel beim Multithreading darin, eine Aufgabe für den Benutzer zu erledigen, ohne die Zeit des Benutzers zu verschwenden.
Die "asynchrone" Alternative
Der "asynchrone Ansatz" ändert dieses Bild, indem Kontextwechsel innerhalb eines einzelnen Threads aktiviert werden. Dies stellt sicher, dass alle unsere Aufgaben auf einer einzelnen CPU ausgeführt werden und bietet möglicherweise einige bescheidene Leistungsverbesserungen in Bezug auf weniger Erstellung / Bereinigung von Threads und weniger echte Kontextwechsel zwischen Threads.
Anstatt einen neuen Thread zu erstellen, um auf den Empfang einer Netzwerkressource zu warten (z. B. Herunterladen eines Bildes), wird eine async
Methode verwendet, bei der await
das Bild verfügbar wird und in der Zwischenzeit der aufrufenden Methode nachgibt.
Der Hauptvorteil hierbei ist, dass Sie sich keine Gedanken über Threading-Probleme wie das Vermeiden von Deadlocks machen müssen, da Sie überhaupt keine Sperren und Synchronisierungen verwenden und der Programmierer weniger Zeit für das Einrichten des Hintergrundthreads und das Zurückkehren hat auf dem UI-Thread, wenn das Ergebnis zurückkommt, um die Benutzeroberfläche sicher zu aktualisieren.
Ich habe mich nicht zu sehr mit den technischen Details befasst, aber ich habe den Eindruck, dass das Verwalten des Downloads mit gelegentlicher geringer CPU-Aktivität nicht zu einer Aufgabe für einen separaten Thread wird, sondern eher zu einer Aufgabe in der Ereigniswarteschlange der Benutzeroberfläche Wenn der Download abgeschlossen ist, wird die asynchrone Methode aus dieser Ereigniswarteschlange fortgesetzt. Mit anderen Worten await
bedeutet so etwas wie "Überprüfen Sie, ob das von mir benötigte Ergebnis verfügbar ist. Wenn nicht, versetzen Sie mich zurück in die Task-Warteschlange dieses Threads".
Beachten Sie, dass dieser Ansatz das Problem einer CPU-intensiven Aufgabe nicht lösen würde: Es sind keine Daten zu erwarten, sodass wir nicht die erforderlichen Kontextwechsel durchführen können, ohne einen tatsächlichen Hintergrund-Worker-Thread zu erstellen. Natürlich kann es immer noch praktisch sein, eine asynchrone Methode zu verwenden, um den Hintergrund-Thread zu starten und das Ergebnis in einem Programm zurückzugeben, das den asynchronen Ansatz verwendet.
Multithreading für Leistung
Da Sie über "Leistung" sprechen, möchte ich auch diskutieren, wie Multithreading für Leistungssteigerungen verwendet werden kann, was mit dem asynchronen Singlethread-Ansatz völlig unmöglich ist.
Wenn Sie tatsächlich in einer Situation sind, in der Sie auf einer einzelnen CPU nicht genügend CPU-Leistung haben und Multithreading für die Leistung verwenden möchten, ist dies häufig schwierig. Auf der anderen Seite ist eine CPU, die nicht ausreicht, häufig auch die einzige Lösung, die es Ihrem Programm ermöglicht, in einem angemessenen Zeitrahmen das zu tun, was Sie erreichen möchten, was die Arbeit lohnt.
Triviale Parallelität
Natürlich kann es manchmal einfach sein, durch Multithreading eine echte Beschleunigung zu erzielen.
Wenn Sie eine große Anzahl unabhängiger rechenintensiver Aufgaben haben (dh Aufgaben, deren Eingabe- und Ausgabedaten im Hinblick auf die Berechnungen, die zur Ermittlung des Ergebnisses durchgeführt werden müssen, sehr klein sind), können Sie häufig eine erhebliche Beschleunigung erzielen Erstellen eines Pools von Threads (entsprechend der Anzahl der verfügbaren CPUs dimensioniert) und Verteilen der Arbeit und Sammeln der Ergebnisse durch einen Master-Thread.
Praktisches Multithreading für mehr Leistung
Ich möchte mich nicht als zu vielfacher Experte ausgeben, aber ich habe den Eindruck, dass das derzeit praktischste Multithreading für die Leistung darin besteht, nach Stellen in einer Anwendung mit trivialer Parallelität zu suchen und mehrere Threads zu verwenden von den Vorteilen profitieren.
Wie bei jeder Optimierung ist es normalerweise besser, zu optimieren, nachdem Sie das Leistungsprofil Ihres Programms erstellt und die Hotspots identifiziert haben: Es ist einfach, ein Programm zu verlangsamen, indem Sie willkürlich entscheiden, dass dieser Teil in einem Thread und dieser Teil in einem anderen ohne ausgeführt werden soll Ermitteln Sie zunächst, ob beide Teile einen erheblichen Teil der CPU-Zeit in Anspruch nehmen.
Ein zusätzlicher Thread bedeutet mehr Einrichtungs- / Abbaukosten und entweder mehr Kontextwechsel oder mehr Kommunikationskosten zwischen CPUs. Wenn es nicht genug Arbeit leistet, um diese Kosten auszugleichen, wenn es sich um eine separate CPU handelt, und aus Gründen der Reaktionsfähigkeit kein separater Thread sein muss, wird es die Dinge ohne Nutzen verlangsamen.
Suchen Sie nach Aufgaben, die nur wenige Abhängigkeiten aufweisen und einen erheblichen Teil der Laufzeit Ihres Programms beanspruchen.
Wenn sie keine gegenseitigen Abhängigkeiten aufweisen, handelt es sich um eine triviale Parallelität. Sie können jede einfach mit einem Thread einrichten und von den Vorteilen profitieren.
Wenn Sie Aufgaben mit begrenzter gegenseitiger Abhängigkeit finden, die durch Sperren und Synchronisieren zum Austauschen von Informationen nicht wesentlich verlangsamt werden, kann Multithreading zu einer Beschleunigung führen, vorausgesetzt, Sie vermeiden die Gefahr eines Deadlocks aufgrund fehlerhafter Logik bei der Synchronisierung oder falsche Ergebnisse, da nicht synchronisiert wird, wenn dies erforderlich ist.
Alternativ suchen einige der gängigsten Anwendungen für Multithreading (in gewissem Sinne) nicht nach einer Beschleunigung eines vorgegebenen Algorithmus, sondern nach einem größeren Budget für den Algorithmus, den sie schreiben möchten: wenn Sie eine Game-Engine schreiben Wenn Ihre KI eine Entscheidung innerhalb Ihrer Bildrate treffen muss, können Sie Ihrer KI häufig ein größeres CPU-Zyklusbudget zuweisen, wenn Sie ihr eine eigene CPU zuweisen können.
Stellen Sie jedoch sicher, dass Sie die Threads profilieren und sicherstellen, dass sie genug Arbeit leisten, um die Kosten zu einem bestimmten Zeitpunkt auszugleichen.
Parallele Algorithmen
Es gibt auch viele Probleme, die durch die Verwendung mehrerer Prozessoren beschleunigt werden können, die jedoch zu monolithisch sind, um sie einfach zwischen CPUs aufzuteilen.
Parallele Algorithmen müssen sorgfältig auf ihre Big-O-Laufzeiten hin analysiert werden, da es für die Kommunikationskosten zwischen den CPUs sehr einfach ist, die Vorteile der Verwendung mehrerer CPUs zu eliminieren. Im Allgemeinen müssen sie weniger Kommunikation zwischen CPUs (in Big-O-Begriffen) verwenden, als sie Berechnungen für jede CPU verwenden.
Im Moment ist es immer noch ein Raum für akademische Forschung, zum Teil wegen der komplexen Analyse, zum Teil, weil triviale Parallelität weit verbreitet ist, zum Teil, weil wir noch nicht so viele CPU-Kerne auf unseren Computern haben, die Probleme haben, die kann nicht in einem vernünftigen Zeitrahmen auf einer CPU gelöst werden könnte mit allen unseren CPUs in einem vernünftigen Zeitrahmen gelöst werden.