Dieser Artikel bietet eine Einführung in die Parallelverarbeitung. Es wird auch die neue parallele Ausführungseinheit Ractor erwähnt, die in Ruby entwickelt wird.
Zunächst werde ich die Begriffe zusammenfassen, die bei der Erörterung dieses Themas häufig verwechselt werden.
Bei der ** Parallelverarbeitung ** werden zu einem bestimmten Zeitpunkt mehrere Prozesse gleichzeitig ausgeführt. ** Parallelverarbeitung ** verarbeitet mehrere Prozesse in der Reihenfolge nach Zeitteilung. Im Gegensatz zur Parallelverarbeitung wird zu einem bestimmten Zeitpunkt immer nur ein Prozess gleichzeitig ausgeführt.
Wenn der Zeitpunkt, zu dem mehrere Prozesse ausgeführt werden, in chronologischer Reihenfolge angezeigt wird, wird das unten gezeigte Bild erhalten. (Die Verarbeitung wird nur in dem Teil mit der blauen Linie ausgeführt.)
Dieser Artikel befasst sich mit dem Verhalten der Parallelverarbeitung. Beachten Sie jedoch, dass sich Code, der für die Parallelverarbeitung geschrieben wird, möglicherweise wie Parallelverarbeitung verhält. (Beispielsweise kann eine 1-Kern-CPU nicht zwei oder mehr Prozesse parallel betreiben.) Das Betriebssystem und die VM planen diesen Bereich auf gute Weise.
Im Allgemeinen gibt es zwei Hauptmethoden, um eine parallele Verarbeitung zu erreichen: ** Multiprozess ** und ** Multithread **. Multi-Prozess ist eine Methode zum Erstellen mehrerer Prozesse und zum Ausführen von jeweils einem Prozess. Multithreading ist eine Methode zum Erstellen mehrerer Threads in einem Prozess und zum Ausführen von jeweils einem Prozess.
Bei mehreren Prozessen wird der Speicherplatz in jedem Prozess getrennt. Daher ist es grundsätzlich unmöglich, Variablen zwischen Prozessen zu übergeben. Es ist auch sehr sicher, da es unbeabsichtigte speichervermittelte Interaktionen zwischen Prozessen verhindert. Der Nachteil ist, dass jeder Prozess über einen Speicherplatz verfügt, sodass die Gesamtspeicherauslastung tendenziell zunimmt. (Unter Linux wird der Speicher zwischen den Prozessen jedoch durch den Mechanismus "Beim Schreiben kopieren" so weit wie möglich gemeinsam genutzt.)
Beim Multithreading verfügt ein Prozess über mehrere Threads, sodass der Speicherplatz von den Threads gemeinsam genutzt wird. Daher kann die Speichernutzung unterdrückt werden, und je nach Implementierung ist das Erstellen und Umschalten von Threads einfacher als das Erstellen und Umschalten von Prozessen. Da sich Threads jedoch gegenseitig über den Speicher beeinflussen können, treten häufig Fehler wie Datenkonflikte auf. Im Allgemeinen muss die Multithread-Programmierung viele Dinge berücksichtigen und ist schwierig korrekt zu implementieren.
Die Einheit, in der ein Prozess in paralleler Verarbeitung ausgeführt wird, wird als ** parallele Ausführungseinheit ** bezeichnet. Im Fall von mehreren Prozessen ist die parallele Ausführungseinheit ein Prozess, und im Fall von mehreren Threads ist es ein Thread.
Es gibt zwei Hauptimplementierungsmethoden für die Thread-Verarbeitung: ** nativer Thread ** und ** grüner Thread **. Der native Thread ist eine Methode zur Realisierung der Multithread-Verarbeitung unter Verwendung der Betriebssystemimplementierung. Da es Sache des Betriebssystems ist, Threads zu planen (zu entscheiden, welcher Thread ausgeführt werden soll), wird die Implementierung des Verarbeitungssystems einfach. Auf der anderen Seite gibt es einen Nachteil, dass die Verarbeitung der Thread-Erstellung und -Schaltung (sogenannter Kontextwechsel) schwer ist. (Übrigens ist nativer Thread ein Konzept, das Kernel-Thread und Lightweight-Prozess kombiniert, aber Details werden weggelassen. Ich bin der Meinung, dass nativer Thread und Kernel-Thread häufig gemischt werden.)
Der grüne Thread ist ein Thread, der ursprünglich von einer sprachverarbeitenden virtuellen Maschine implementiert wurde (z. B. yarv of cruby, jvm of java usw.), und ist eine Methode zur Realisierung der Multi-Thread-Verarbeitung. Die Goroutine von Golang ist auch eine Art grüner Faden, und ihre Leichtigkeit ist zu berühmt. In Cruby wurde es vor 1.9 von Green Thread implementiert, aber jetzt wurde es geändert, um nativen Thread zu verwenden. Grüne Threads werden auch als Benutzer-Threads bezeichnet.
Als Beispiel wird die Implementierung der Parallelverarbeitung in Ruby gezeigt. In Ruby können Sie die Parallelverarbeitung mithilfe des Edelsteins Parallel einfach beschreiben.
Der Multiprozesscode sieht folgendermaßen aus:
multi_process.rb
require 'parallel'
Parallel.each(1..10, in_processes: 10) do |i|
sleep 10
puts i
end
Wenn Sie diesen Code ausführen und die Prozessliste anzeigen, sieht er folgendermaßen aus: Sie können sehen, dass ein Hauptprozess und 10 untergeordnete Prozesse stattfinden.
$ ps aux | grep ruby
PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND PRI STIME UTIME
79050 9.7 0.1 4355568 14056 s005 S+ 2:39PM 0:00.28 ruby mp.rb
79072 0.0 0.0 4334968 1228 s005 S+ 2:39PM 0:00.00 ruby mp.rb
79071 0.0 0.0 4334968 1220 s005 S+ 2:39PM 0:00.00 ruby mp.rb
79070 0.0 0.0 4334968 1244 s005 S+ 2:39PM 0:00.00 ruby mp.rb
79069 0.0 0.0 4334968 1244 s005 S+ 2:39PM 0:00.00 ruby mp.rb
79068 0.0 0.0 4334968 1172 s005 S+ 2:39PM 0:00.00 ruby mp.rb
79067 0.0 0.0 4334968 1180 s005 S+ 2:39PM 0:00.00 ruby mp.rb
79066 0.0 0.0 4334968 1208 s005 S+ 2:39PM 0:00.00 ruby mp.rb
79065 0.0 0.0 4334968 1252 s005 S+ 2:39PM 0:00.00 ruby mp.rb
79064 0.0 0.0 4334968 1168 s005 S+ 2:39PM 0:00.00 ruby mp.rb
79063 0.0 0.0 4334968 1168 s005 S+ 2:39PM 0:00.00 ruby mp.rb
Der Multithread-Code sieht folgendermaßen aus:
multi_threads.rb
require 'parallel'
Parallel.each(1..10, in_threads: 10) do |i|
sleep 10
puts i
end
Schauen Sie sich auch hier die Thread-Liste an.
Wenn Sie dem Befehl "ps" "-L" hinzufügen, wird der Thread wie ein Prozess angezeigt.
Ohne -L
gibt es nur einen Prozess, aber mit -L
werden 11 Zeilen angezeigt.
Darüber hinaus zeigt die Spalte "NLWP" die Anzahl der Threads im Prozess an. Da dies 11 ist (Hauptthread x1 + Arbeitsthread x10), ist ersichtlich, dass die Verarbeitung Multithreading ist.
$ ps aux | grep mt.rb
4419 1.0 0.6 850176 12384 pts/1 Sl+ 15:41 0:00 ruby mt.rb
$ ps aux -L | grep mt.rb
PID LWP %CPU NLWP %MEM VSZ RSS TTY STAT START TIME COMMAND
4419 4419 6.0 11 0.6 850176 12384 pts/1 Sl+ 15:41 0:00 ruby mt.rb
4419 4453 0.0 11 0.6 850176 12384 pts/1 Sl+ 15:41 0:00 ruby mt.rb
4419 4454 0.0 11 0.6 850176 12384 pts/1 Sl+ 15:41 0:00 ruby mt.rb
4419 4455 0.0 11 0.6 850176 12384 pts/1 Sl+ 15:41 0:00 ruby mt.rb
4419 4456 0.0 11 0.6 850176 12384 pts/1 Sl+ 15:41 0:00 ruby mt.rb
4419 4457 0.0 11 0.6 850176 12384 pts/1 Sl+ 15:41 0:00 ruby mt.rb
4419 4458 0.0 11 0.6 850176 12384 pts/1 Sl+ 15:41 0:00 ruby mt.rb
4419 4460 0.0 11 0.6 850176 12384 pts/1 Sl+ 15:41 0:00 ruby mt.rb
4419 4461 0.0 11 0.6 850176 12384 pts/1 Sl+ 15:41 0:00 ruby mt.rb
4419 4462 0.0 11 0.6 850176 12384 pts/1 Sl+ 15:41 0:00 ruby mt.rb
4419 4463 0.0 11 0.6 850176 12384 pts/1 Sl+ 15:41 0:00 ruby mt.rb
Bei der Multithread-Verarbeitung können verschiedene Probleme auftreten, da die Verarbeitung parallel zu mehreren Threads ausgeführt wird, die sich einen Speicher teilen. Eines der Hauptprobleme ist ** Data Racing **.
Datenrennen können mit Code wie dem folgenden stattfinden. Dieser Code versucht, die Summe der Ganzzahlen von 1 bis 10 zu ermitteln. Aufgrund von Datenrennproblemen ist es jedoch möglicherweise nicht möglich, die Summe korrekt zu ermitteln.
require 'parallel'
sum = 0;
Parallel.each(1..10, in_threads: 10) do |i|
add = sum + i
sum = add
end
puts sum
In diesem Code teilt jeder Thread die Variable "sum" und jeder Thread liest und schreibt gleichzeitig sum. Infolgedessen kann der in einem Thread geschriebene Inhalt von einem anderen Thread überschrieben werden. Daher besteht das Problem, dass der obige Code die Summe möglicherweise nicht normal berechnen kann.
Ein üblicher Weg, um Probleme mit Datenrennen zu lösen, besteht darin, exklusive Sperren zwischen Threads vorzunehmen.
require 'parallel'
sum = 0;
m = Mutex.new
Parallel.each(1..10, in_threads: 10) do |i|
m.lock
add = sum + i
sum = add
m.unlock
end
puts sum
Infolgedessen wird jeweils nur ein Thread ausgeführt, während die Sperre gehalten wird, und das Datenrennen wird eliminiert.
Code, der diese Probleme richtig berücksichtigt und beim Multithreading gut funktioniert, wird als "thread-sicher" bezeichnet.
** GIL ** wird häufig in der Multithread-Verarbeitung in leichtgewichtigen Sprachen (Ruby, Python usw.) erwähnt. In Ruby heißt es übrigens GVL (Giant VM Lock).
GIL verhindert, dass mehrere Threads gleichzeitig ausgeführt werden, indem eine exklusive Steuerung zwischen Threads durchgeführt wird. Mit anderen Worten, es kann immer nur ein Thread gleichzeitig in einem Interpreter und einer VM ausgeführt werden. Gründe und Vorteile dieses Bedarfs sind:
Dank GIL funktioniert der gerade illustrierte Ruby-Code für die Multithread-Programmierung ohne Mutex einwandfrei. Dieses Verhalten unterscheidet sich nicht von Rubys grundlegender Idee, die Programmierung zu vereinfachen.
Die Tatsache, dass jeweils nur ein Thread ausgeführt werden kann, bedeutet jedoch, dass die ursprüngliche Parallelverarbeitung nicht möglich ist. Aus diesem Grund wird häufig erwähnt, dass Ruby und Python nicht für die parallele Berechnung geeignet sind.
In Ausnahmefällen bricht der Thread beim Warten auf E / A die GIL ab, sodass mehrere Threads gleichzeitig die Verarbeitung ausführen können. Aus diesem Grund wird bei einer Verarbeitung, die viel E / A-Wartezeit erfordert (Webserver usw.), Multithreading praktisch sogar in einem Verarbeitungssystem mit GIL verwendet.
Da HTTP-Server normalerweise jede Anforderung gleichzeitig verarbeiten müssen, wird häufig eine parallele Verarbeitung implementiert. Typische HTTP-Server in Ruby sind ** Einhorn ** und ** Puma **, ersteres ist eine Multiprozess-Implementierung und letzteres ist eine Multithread-Implementierung.
Die Leistung von Einhorn und Puma wird in [diesem Blog] verglichen (https://deliveroo.engineering/2016/12/21/unicorn-vs-puma-rails-server-benchmarks.html).
Das Fazit dieses Blogs lautet wie folgt:
Dies ist selbst unter Berücksichtigung des obigen Mechanismus ein überzeugendes Ergebnis.
Bisher haben wir erklärt, wie Parallelverarbeitung realisiert werden kann, und die Implementierung in Ruby und ihre Leistung gezeigt. Bei der Multithread-Verarbeitung in Ruby besteht das Problem, dass die ursprüngliche Leistung aufgrund von GVL nicht erreicht werden kann. Ractor (ehemals Guild) ist ein neuer Ruby-Parallelverarbeitungsmechanismus, der zur Lösung dieses Problems entwickelt wurde.
Ractor kann eine echte Multithread-Leistung erzielen und gleichzeitig den Vorteil beibehalten, dass die herkömmliche GVL-Multithread-Programmierung einfacher zu handhaben ist.
Ich werde den Mechanismus erklären.
Datenrennen treten auf, weil mehrere Threads in eine einzelne Variable lesen und schreiben können, da sich die Threads den Speicher teilen. Der Weg, dies zu lösen, ist
In Ractor wurden drei Methoden angewendet. Diese neue parallele Ausführungseinheit heißt Ractor. Ein Ruby-Prozess hat einen oder mehrere Ractors und ein Ractor hat einen oder mehrere Threads. Da jeder Ractor in einem separaten Speicherbereich arbeitet, ist es kein Problem, Speicher wie in herkömmlichen Threads gemeinsam zu nutzen.
Quelle: https://www.slideshare.net/KoichiSasada/guild-prototype
Darüber hinaus kann Ruby-Code vor der Einführung von Ractor die Abwärtskompatibilität aufrechterhalten, indem er in einem Ractor ausgeführt wird.
Da Ractors keinen gemeinsamen Speicher haben, ist es möglicherweise umständlich, Informationen weiterzugeben. Um dies zu lösen, gibt es auch eine Funktion namens "Kanal", die die Kommunikation zwischen Ractors realisiert. Objekte, die Sie freigeben möchten, können nur über "Kanal" übergeben werden.
Objekte werden in ** gemeinsam nutzbare Objekte ** und ** nicht gemeinsam nutzbare Objekte ** eingeteilt.
Ein gemeinsam nutzbares Objekt ist ein Objekt, z. B. eine schreibgeschützte Konstante, das keine Datenrennen verursachen kann, selbst wenn es von Faktoren gemeinsam genutzt wird. Gemeinsam nutzbare Objekte können über den Kanal frei geteilt werden.
Nicht gemeinsam nutzbare Objekte beziehen sich auf allgemeine veränderbare Objekte. Wenn Sie dieses Objekt durch einen Kanal führen, wird die Semantik tief kopiert oder verschoben. Im Fall von Deep Copy steigen die Kosten für die Kopierverarbeitung und die Speichernutzung, aber es ist genauso sicher und leicht zu verstehen wie Multiprozesse. Bei der Verschiebungssemantik wird das Eigentum an dem Objekt auf einen anderen Faktor übertragen. Daher kann der ursprüngliche Ractor nicht auf das Objekt verweisen, aber im Gegensatz zu Deep Copy steigen die Verarbeitungskosten und die Speichernutzung nicht so stark wie beim Kopieren.
Zusammenfassung:
Auf diese Weise realisiert Ractor eine einfache Multithread-Programmierung bei gleichzeitiger Wahrung der Thread-Sicherheit.
Ractor ist eine parallele Ausführungseinheit zwischen Prozessen und Threads. Durch die richtige Auswahl der Informationen, die der Entwickler zwischen Ractors teilen möchte, kann eine parallele Verarbeitung realisiert werden, ohne die RAM-Nutzung im Vergleich zu Multiprozessen zu erhöhen und ohne die Leistung aufgrund von GIL zu beeinträchtigen.
Ractor erhält als neues Feature in Ruby 3 viel Aufmerksamkeit. Es scheint, dass sich Ractor selbst noch in der Entwicklung befindet, und es wird eine Weile dauern, bis es die Reichweite allgemeiner Ruby-Benutzer erreicht. In Zukunft wird erwartet, dass die Ruby-Multithread-Bibliothek in Ractor erneut implementiert wird. Es kann nahe an der Zeit sein, in der HTTP-Server, die Puma ersetzen, zum Mainstream werden.
Dies ist ein Satz, der für das Studium zusammengefasst wurde. Ich würde mich freuen, wenn Sie auf Fehler hinweisen könnten!
Recommended Posts