it-swarm.com.de

Warum wird Multithreading häufig zur Leistungsverbesserung bevorzugt?

Ich habe eine Frage, es geht darum, warum Programmierer Parallelität und Multithread-Programme im Allgemeinen zu lieben scheinen.

Ich betrachte hier zwei Hauptansätze:

  • ein asynchroner Ansatz, der im Wesentlichen auf Signalen basiert, oder nur ein asynchroner Ansatz, wie er von vielen Artikeln und Sprachen wie dem neuen C # 5.0 aufgerufen wird, und ein "Companion-Thread", der die Richtlinien Ihrer Pipeline verwaltet
  • ein gleichzeitiger Ansatz oder ein Multithreading-Ansatz

Ich werde nur sagen, dass ich über die Hardware hier und das Worst-Case-Szenario nachdenke und diese beiden Paradigmen selbst getestet habe. Das asynchrone Paradigma ist ein Gewinner an dem Punkt, an dem ich nicht verstehe, warum Menschen 90% der Zeit Sprechen Sie über Multithreading, wenn sie Dinge beschleunigen oder ihre Ressourcen gut nutzen möchten.

Ich habe Multithread-Programme und Async-Programme auf einem alten Computer mit einem Intel Quad-Core getestet, der keinen Speichercontroller in der CPU bietet. Der Speicher wird vollständig vom Motherboard verwaltet. In diesem Fall sind die Leistungen mit einem schrecklich Multithread-Anwendung, selbst eine relativ geringe Anzahl von Threads wie 3-4-5 kann ein Problem sein, die Anwendung reagiert nicht und ist nur langsam und unangenehm.

Ein guter asynchroner Ansatz ist auf der anderen Seite wahrscheinlich nicht schneller, aber auch nicht schlecht. Meine Anwendung wartet nur auf das Ergebnis und hängt nicht, sie reagiert und es gibt eine viel bessere Skalierung.

Ich habe auch festgestellt, dass eine Kontextänderung in der Threading-Welt im realen Szenario nicht so billig ist, sondern in der Tat ziemlich teuer, insbesondere wenn Sie mehr als 2 Threads haben, die zyklisch und untereinander ausgetauscht werden müssen, um berechnet zu werden.

Bei modernen CPUs ist die Situation nicht wirklich anders, der Speichercontroller ist integriert, aber mein Punkt ist, dass eine x86-CPU im Grunde eine serielle Maschine ist und die Speichersteuerung genauso funktioniert wie bei der alten Maschine mit einem externen Speichercontroller auf dem Motherboard . Der Kontextwechsel ist immer noch ein relevanter Kostenfaktor in meiner Anwendung und die Tatsache, dass der Speichercontroller integriert ist oder dass die neuere CPU mehr als 2 Kerne hat, ist für mich kein Schnäppchen.

Für das, was ich erlebt habe, ist der gleichzeitige Ansatz theoretisch gut, aber in der Praxis nicht so gut. Mit dem von der Hardware auferlegten Speichermodell ist es schwierig, dieses Paradigma gut zu nutzen, und es führt auch viele Probleme ein, die von der Verwendung reichen meiner Datenstrukturen zum Join mehrerer Threads.

Außerdem bieten beide Paradigmen keine Sicherheit, wenn die Aufgabe oder die Arbeit zu einem bestimmten Zeitpunkt erledigt wird, was sie aus funktionaler Sicht wirklich ähnlich macht.

Warum schlägt die Mehrheit der Benutzer laut X86-Speichermodell vor, Parallelität mit C++ und nicht nur einen asynchronen Ansatz zu verwenden? Warum nicht auch das Worst-Case-Szenario eines Computers in Betracht ziehen, bei dem der Kontextwechsel wahrscheinlich teurer ist als die Berechnung selbst?

23
user1849534

Sie haben mehrere Kerne/Prozessoren, verwenden sie

Async is am besten für schwere IO gebundene Verarbeitung, aber was ist mit schwerer CPU-gebundener Verarbeitung?

Das Problem tritt auf, wenn Code-Blöcke mit einem Thread bei einem lang laufenden Prozess blockiert werden (dh hängen bleiben). Denken Sie beispielsweise daran, dass beim Drucken eines Textverarbeitungsdokuments die gesamte Anwendung einfrieren würde, bis der Auftrag gesendet wurde. Das Einfrieren von Anwendungen ist ein Nebeneffekt einer Blockierung von Single-Thread-Anwendungen während einer CPU-intensiven Aufgabe.

In einer Multithread-Anwendung können CPU-intensive Aufgaben (z. B. ein Druckauftrag) an einen Hintergrund-Worker-Thread gesendet werden, wodurch der UI-Thread freigegeben wird.

Ebenso kann in einer Multiprozessanwendung der Job per Messaging (z. B. IPC, Sockets usw.) an einen Unterprozess gesendet werden, der speziell für die Verarbeitung von Jobs entwickelt wurde.

In der Praxis hat asynchroner und Multithread-/Prozesscode jeweils Vor- und Nachteile.

Sie können den Trend auf den wichtigsten Cloud-Plattformen sehen, da sie Instanzen anbieten, die auf CPU-gebundene Verarbeitung spezialisiert sind, und Instanzen, die auf IO gebundene Verarbeitung) spezialisiert sind.

Beispiele :

  • Der Speicher (ex Amazon S3, Google Cloud Drive) ist CPU-gebunden
  • Webserver sind IO gebunden (Amazon EC2, Google App Engine)
  • Datenbanken sind sowohl CPU-gebunden für Schreibvorgänge/Indizierung als auch IO gebunden für Lesevorgänge)

Um es ins rechte Licht zu rücken ...

Ein Webserver ist ein perfektes Beispiel für eine Plattform, die stark IO) gebunden ist. Ein Multithread-Webserver, der einen Thread pro Verbindung zuweist, lässt sich nicht gut skalieren, da jeder Thread aufgrund der Erhöhung mehr Overhead verursacht Umfang der Kontextumschaltung und Thread-Sperre für gemeinsam genutzte Ressourcen. Während ein asynchroner Webserver einen einzelnen Adressraum verwenden würde.

Ebenso würde eine auf das Codieren von Videos spezialisierte Anwendung in einer Multithread-Umgebung viel besser funktionieren, da die umfangreiche Verarbeitung den Haupt-Thread sperren würde, bis die Arbeit erledigt ist. Es gibt Möglichkeiten, dies zu verringern, aber es ist viel einfacher, wenn ein einzelner Thread eine Warteschlange verwaltet, ein zweiter Thread die Bereinigung verwaltet und ein Pool von Threads die umfangreiche Verarbeitung verwaltet. Die Kommunikation zwischen Threads erfolgt nur, wenn Aufgaben zugewiesen/abgeschlossen wurden, sodass der Overhead für die Thread-Sperre auf ein Minimum beschränkt bleibt.

Die beste Anwendung verwendet häufig eine Kombination aus beiden. Eine Webanwendung kann beispielsweise nginx (dh asynchrones Single-Threaded) als Load Balancer verwenden, um den Torrent eingehender Anforderungen zu verwalten, einen ähnlichen asynchronen Webserver (ex Node.js) zur Verarbeitung von http-Anforderungen und eine Reihe von Multithread-Servern das Hochladen/Streamen/Codieren von Inhalten usw.

Im Laufe der Jahre gab es viele Religionskriege zwischen Multithread-, Multiprozess- und Async-Modellen. Wie bei den meisten Dingen sollte die beste Antwort wirklich sein: "Es kommt darauf an."

Es folgt der gleichen Denkweise, die die parallele Verwendung von GPU- und CPU-Architekturen rechtfertigt. Zwei spezialisierte Systeme, die zusammen laufen, können eine viel größere Verbesserung aufweisen als ein einzelner monolithischer Ansatz.

Keiner ist besser, weil beide ihren Nutzen haben. Verwenden Sie das beste Werkzeug für den Job.

Update:

Ich habe den Verweis auf Apache entfernt und eine kleine Korrektur vorgenommen. Apache verwendet ein Multiprozessmodell, das für jede Anforderung einen Prozess gibtelt, wodurch der Umfang der Kontextumschaltung auf Kernelebene erhöht wird. Da der Speicher nicht prozessübergreifend gemeinsam genutzt werden kann, entstehen für jede Anforderung zusätzliche Speicherkosten.

Multithreading erfordert zusätzlichen Speicher, da es auf einem gemeinsam genutzten Speicher zwischen Threads basiert. Der gemeinsam genutzte Speicher entfernt den zusätzlichen Speicheraufwand, führt jedoch immer noch zu einer erhöhten Kontextumschaltung. Um sicherzustellen, dass keine Race-Bedingungen auftreten, sind außerdem Thread-Sperren (die den exklusiven Zugriff auf jeweils nur einen Thread gewährleisten) für alle Ressourcen erforderlich, die von mehreren Threads gemeinsam genutzt werden.

Es ist lustig, dass Sie sagen: "Programmierer scheinen Parallelität und Multithread-Programme im Allgemeinen zu lieben." Multithread-Programmierung wird allgemein von jedem gefürchtet, der in seiner Zeit eine beträchtliche Menge davon getan hat. Dead Locks (ein Fehler, der auftritt, wenn eine Ressource fälschlicherweise von zwei verschiedenen Quellen gesperrt wird, die beide daran hindern, jemals fertig zu werden) und Race-Bedingungen (wobei das Programm fälschlicherweise das falsche Ergebnis ausgibt zufällig aufgrund falscher Sequenzierung) sind einige der am schwierigsten aufzuspüren und zu beheben.

Update2:

Im Gegensatz zu der pauschalen Aussage, dass IPC schneller als die Netzwerkkommunikation (dh Socket) ist. Dies ist nicht immer der Fall . Beachten Sie, dass dies Verallgemeinerungen und implementierungsspezifisch sind Details können einen großen Einfluss auf das Ergebnis haben.

34
Evan Plaice

Microsofts asynchroner Ansatz ist ein guter Ersatz für die häufigsten Zwecke der Multithread-Programmierung: Verbesserung der Reaktionsfähigkeit in Bezug auf IO Aufgaben).

Es ist jedoch wichtig zu wissen, dass der asynchrone Ansatz die Leistung oder die Reaktionsfähigkeit in Bezug auf CPU-intensive Aufgaben überhaupt nicht verbessern kann.

Multithreading für Reaktionsfähigkeit

Multithreading für Reaktionsfähigkeit ist die traditionelle Methode, um ein Programm während schwerer IO Aufgaben oder schwerer Rechenaufgaben) ansprechbar zu halten. Sie speichern Dateien in einem Hintergrund-Thread, damit der Benutzer seine Arbeit fortsetzen kann, ohne dies tun zu müssen Warten Sie, bis die Festplatte ihre Aufgabe beendet hat. Der Thread IO] blockiert häufig das Warten auf den Abschluss eines Teils eines Schreibvorgangs, sodass häufig Kontextwechsel durchgeführt werden.

In ähnlicher Weise möchten Sie bei der Durchführung einer komplexen Berechnung eine regelmäßige Kontextumschaltung zulassen, damit die Benutzeroberfläche weiterhin reagiert und der Benutzer nicht glaubt, dass das Programm abgestürzt ist.

Das Ziel hier ist im Allgemeinen nicht, dass mehrere Threads auf verschiedenen CPUs ausgeführt werden. Stattdessen sind wir nur daran interessiert, Kontextwechsel zwischen der lang laufenden Hintergrundaufgabe und der Benutzeroberfläche durchzuführen, damit die Benutzeroberfläche den Benutzer aktualisieren und darauf reagieren 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 Betriebssystem entscheidet 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 es ist uns egal, da die Leistung der CPU nicht unser Ziel war. Wir wissen, dass wir normalerweise mehr CPU-Leistung haben, als wir benötigen. Daher ist es unser Ziel beim Multithreading, 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 garantiert, dass alle unsere Aufgaben auf einer einzelnen CPU ausgeführt werden, und bietet möglicherweise einige bescheidene Leistungsverbesserungen in Bezug auf weniger Thread-Erstellung/Bereinigung und weniger reale Kontextwechsel zwischen Threads.

Anstatt einen neuen Thread zu erstellen, der auf den Empfang einer Netzwerkressource wartet (z. B. das Herunterladen eines Bildes), wird eine async -Methode verwendet, mit der await das Bild verfügbar wird und in der Zwischenzeit gibt der aufrufenden Methode nach.

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 keine Synchronisierung verwenden und der Programmierer weniger Arbeit für das Einrichten des Hintergrund-Threads und das Zurückkehren benötigt auf dem UI-Thread, wenn das Ergebnis zurückkommt, um die UI 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 gelegentlich geringer CPU-Aktivität nicht zu einer Aufgabe für einen separaten Thread wird, sondern eher zu einer Aufgabe in der UI-Ereigniswarteschlange, und wann die Wenn der Download abgeschlossen ist, wird die asynchrone Methode aus dieser Ereigniswarteschlange fortgesetzt. Mit anderen Worten, await bedeutet so etwas wie "Überprüfen, ob das von mir benötigte Ergebnis verfügbar ist, wenn nicht, setzen Sie mich wieder in die Aufgabenwarteschlange dieses Threads".

Beachten Sie, dass dieser Ansatz das Problem einer CPU-intensiven Aufgabe nicht lösen würde: Es müssen keine Daten erwartet werden, sodass wir nicht die Kontextwechsel erhalten können, die erforderlich sind, ohne einen tatsächlichen Hintergrund-Worker-Thread zu erstellen. Natürlich kann es immer noch zweckmäßig sein, eine asynchrone Methode zu verwenden, um den Hintergrundthread zu starten und das Ergebnis in einem Programm zurückzugeben, das den asynchronen Ansatz allgegenwärtig 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 Single-Thread-Ansatz völlig unmöglich ist.

Wenn Sie sich tatsächlich in einer Situation befinden, in der Sie nicht genügend CPU-Leistung auf einer einzelnen CPU haben und Multithreading für die Leistung verwenden möchten, ist dies häufig schwierig. Wenn andererseits eine CPU nicht über genügend Rechenleistung verfügt, ist dies häufig auch die einzige Lösung, mit der Ihr Programm in einem angemessenen Zeitrahmen das tun kann, was Sie möchten. Daher lohnt sich die Arbeit.

Triviale Parallelität

Natürlich ist es manchmal kann einfach, durch Multithreading eine echte Beschleunigung zu erzielen.

Wenn Sie zufällig eine große Anzahl unabhängiger rechenintensiver Aufgaben haben (d. H. 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 entsprechend dimensioniert) und Verteilen der Arbeit durch einen Master-Thread und Sammeln der Ergebnisse.

Praktisches Multithreading für die Leistung

Ich möchte mich nicht als zu viel Experte ausgeben, aber ich habe den Eindruck, dass das praktischste Multithreading für die Leistung, das heutzutage stattfindet, im Allgemeinen darin besteht, nach Stellen in einer Anwendung zu suchen, die eine triviale Parallelität aufweisen und mehrere Threads verwenden die Vorteile zu ernten.

Wie bei jeder Optimierung ist es normalerweise besser, zu optimieren, nachdem Sie die Leistung Ihres Programms profiliert 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 Stellen Sie zunächst fest, ob beide Teile einen erheblichen Teil der CPU-Zeit beanspruchen.

Ein zusätzlicher Thread bedeutet mehr Einrichtungs-/Abbaukosten und entweder mehr Kontextwechsel oder mehr Kommunikationskosten zwischen den CPUs. Wenn es nicht genug Arbeit macht, um diese Kosten auf einer separaten CPU auszugleichen, 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 Abhängigkeiten aufweisen, handelt es sich um eine triviale Parallelität. Sie können sie einfach mit einem Thread einrichten und die Vorteile nutzen.

Wenn Sie Aufgaben mit begrenzter gegenseitiger Abhängigkeit finden, sodass das Sperren und Synchronisieren zum Austausch von Informationen diese nicht wesentlich verlangsamt, kann Multithreading zu einer gewissen Beschleunigung führen, vorausgesetzt, Sie vermeiden die Gefahren eines Deadlocks aufgrund fehlerhafter Logik beim Synchronisieren oder falsche Ergebnisse, da bei Bedarf keine Synchronisierung durchgeführt wird.

Alternativ suchen einige der gängigsten Anwendungen für Multithreading nicht (in gewissem Sinne) nach einer Beschleunigung eines vorgegebenen Algorithmus, sondern nach einem größeren Budget für den Algorithmus, den sie schreiben möchten: Wenn Sie eine Spiel-Engine schreiben Wenn Ihre KI innerhalb Ihrer Framerate eine Entscheidung treffen muss, können Sie Ihrer KI häufig ein größeres Budget für den CPU-Zyklus geben, wenn Sie ihr eine eigene CPU geben können.

Stellen Sie jedoch sicher, dass Sie die Threads profilieren und sicherstellen, dass sie genug Arbeit leisten, um die Kosten irgendwann auszugleichen.

Parallele Algorithmen

Es gibt auch viele Probleme, die mit mehreren 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 im Hinblick auf den besten verfügbaren nicht parallelen Algorithmus analysiert werden, da es für die Kommunikationskosten zwischen den CPUs sehr einfach ist, die Vorteile der Verwendung mehrerer CPUs auszuschließen. Im Allgemeinen müssen sie weniger Kommunikation zwischen CPUs (in Big-O-Begriffen) verwenden als Berechnungen für jede CPU.

Im Moment ist es noch größtenteils ein Raum für akademische Forschung, teilweise wegen der erforderlichen komplexen Analyse, teilweise weil triviale Parallelität weit verbreitet ist, teilweise weil wir noch nicht so viele CPU-Kerne auf unseren Computern haben, dass Probleme auftreten, die kann nicht in einem angemessenen Zeitrahmen auf einer CPU gelöst werden könnte in einem angemessenen Zeitrahmen mit allen unseren CPUs gelöst werden.

13

die Anwendung reagiert nicht und ist nur langsam und unangenehm.

Und da ist dein Problem. Eine reaktionsfähige Benutzeroberfläche macht keine performante Anwendung. Oft das Gegenteil. Es wird eine Menge Zeit damit verbracht, die Eingabe der Benutzeroberfläche zu überprüfen, anstatt die Arbeitsthreads ihre Arbeit erledigen zu lassen.

Was 'nur' einen asynchronen Ansatz betrifft, so ist dies auch Multithreading, obwohl es für diesen einen speziellen Anwendungsfall optimiert wurde in den meisten Umgebungen. In anderen Fällen erfolgt diese Asynchronisierung über Coroutinen, die ... nicht immer gleichzeitig ausgeführt werden.

Ehrlich gesagt finde ich es schwieriger, über asynchrone Vorgänge nachzudenken und sie so zu verwenden, dass sie tatsächlich Vorteile (Leistung, Robustheit, Wartbarkeit) bieten, selbst im Vergleich zu ... mehr manuellen Ansätzen.

3
Telastyn