Concurrency Guide

Dies ist eine Anleitung zum Verständnis der Nebenläufigkeit im Quellcode von cruby, sei es beim Beitrag zu Ruby durch Schreiben von C oder beim Beitrag zu einem der JITs. Native Erweiterungen werden hier nicht behandelt, nur die Kernsprache. Sie wird Folgendes behandeln:

Was muss synchronisiert werden?

Vor Raktoren konnte nur ein Ruby-Thread gleichzeitig laufen. Das bedeutet jedoch nicht, dass Sie Nebenläufigkeitsprobleme vergessen konnten. Der Timer-Thread ist ein nativer Thread, der mit anderen Ruby-Threads interagiert und einige VM-Interna ändert. Wenn diese Änderungen parallel vom Timer-Thread und einem Ruby-Thread vorgenommen werden können, müssen sie synchronisiert werden.

Wenn Sie Raktoren hinzufügen, wird es komplizierter. Raktoren erlauben es Ihnen jedoch, die Synchronisation für nicht teilbare Objekte zu vergessen, da sie nicht über Raktoren hinweg verwendet werden. Nur ein Ruby-Thread kann das Objekt gleichzeitig berühren. Für teilbare Objekte sind sie tiefgefroren, sodass keine Mutationen an den Objekten selbst stattfinden. Das Lesen/Schreiben von Konstanten über Raktoren hinweg muss jedoch synchronisiert werden. In diesem Fall müssen Ruby-Threads eine konsistente Sicht auf die VM sehen. Wenn die Veröffentlichung des Updates 2 Schritte oder sogar zwei separate Anweisungen erfordert, wie in diesem Fall, ist eine Synchronisation erforderlich.

Die meiste Synchronisation dient dem Schutz von VM-Interna. Diese Interna umfassen Strukturen für den Thread-Scheduler auf jedem Raktor, den globalen Raktor-Scheduler, die Koordination zwischen Ruby-Threads und Raktoren, globale Tabellen (für fstrings, Encodings, Symbole und globale Variablen) usw. Alles, was von einem Raktor mutiert werden kann und gleichzeitig von einem anderen Raktor gelesen oder mutiert werden kann, erfordert eine ordnungsgemäße Synchronisation.

Die VM-Sperre

Es gibt nur eine VM-Sperre, und sie dient kritischen Abschnitten, die nur von einem Raktor gleichzeitig betreten werden können. Ohne Raktoren ist die VM-Sperre nutzlos. Sie verhindert nicht, dass alle Raktoren laufen, da Raktoren auch ohne Erwerb dieser Sperre laufen können. Wenn Sie globale (geteilte) Daten zwischen Raktoren aktualisieren und keine Atomoperationen verwenden, müssen Sie eine Sperre verwenden, und dies ist eine praktische.

Sie können (solange keine anderen Sperren vor der VM-Sperre gehalten werden)

Sie können nicht

Intern ist die VM-Sperre vm->ractor.sync.lock.

Sie müssen sich auf einem Ruby-Thread befinden, um die VM-Sperre zu erwerben. Sie können sie auch nicht innerhalb von Funktionen erwerben, die während des Sweeps aufgerufen werden können, da MMTK auf einem anderen Thread sweepen und Sie einen gültigen ec benötigen, um die Sperre zu ergreifen. Aus demselben Grund (unter anderem) können Sie sie auch nicht vom Timer-Thread aus ergreifen.

Andere Sperren

Alle nativen Sperren, die nicht die VM-Sperre sind, teilen sich einen strengeren Satz von Regeln für das, was während des kritischen Abschnitts erlaubt ist. Mit nativen Sperren meinen wir alles, was rb_native_mutex_lock verwendet. Einige wichtige Sperren sind die interrupt_lock, die Raktor-Scheduling-Sperre (schützt globale Scheduling-Datenstrukturen), die Thread-Scheduling-Sperre (lokal für jeden Raktor, schützt Raktor-Scheduling-Datenstrukturen) und die Raktor-Sperre (lokal für jeden Raktor, schützt Raktor-Datenstrukturen).

Wenn Sie eine dieser Sperren erwerben,

Sie können

Sie können nicht

Unterschied zwischen VM Lock und GVL

Die VM Lock ist eine bestimmte Sperre im Quellcode. Es gibt nur eine VM Lock. Das GVL hingegen ist eher eine Kombination von Sperren. Es wird "erworben", wenn ein Ruby-Thread kurz davor steht zu laufen oder läuft. Da viele Ruby-Threads gleichzeitig laufen können, wenn sie sich in verschiedenen Raktoren befinden, gibt es viele GVLs (1 pro SNT + 1 für den Haupt-Raktor). Es kann nicht mehr als "Global VM Lock" betrachtet werden, wie es früher vor Raktoren der Fall war.

VM-Barrieren

Manchmal reicht das Ergreifen der VM-Sperre nicht aus und Sie benötigen die Garantie, dass alle Raktoren angehalten wurden. Dies geschieht zum Beispiel beim Ausführen eines GC. Um eine Barriere zu erhalten, ergreifen Sie die VM-Sperre und rufen rb_vm_barrier() auf. Solange die VM-Sperre gehalten wird, laufen keine anderen Raktoren. Sie wird nicht oft verwendet, da das Ergreifen einer Barriere die Leistung von Raktoren erheblich verlangsamt, aber es ist nützlich zu wissen und manchmal die einzige Lösung.

Sperrenreihenfolgen

Es ist ratsam, nicht mehr als 2 Sperren gleichzeitig auf demselben Thread zu halten. Das Sperren mehrerer Sperren kann zu Deadlocks führen, tun Sie es also mit Vorsicht. Beim Sperren mehrerer Sperren gleichzeitig folgen Sie einer konsistenten Reihenfolge im Programm, sonst können Sie Deadlocks einführen. Hier sind die Reihenfolgen einiger wichtiger Sperren:

Diese Reihenfolgen können sich ändern, prüfen Sie also den Quellcode, wenn Sie sich nicht sicher sind. Darüber hinaus:

Ruby Interrupt Handling

Wenn die VM Ruby-Code ausführt, überprüfen Ruby-Threads periodisch Ruby-Level-Interrupts. Diese Software-Interrupts dienen verschiedenen Zwecken in Ruby und können von anderen Ruby-Threads oder dem Timer-Thread gesetzt werden.

Dies ist keine vollständige Liste.

Beim Senden eines Interrupts an einen Ruby-Thread kann der Ruby-Thread blockiert sein. Er könnte sich zum Beispiel mitten in einem Aufruf von TCPSocket#read befinden. Wenn dies der Fall ist, wird die ubf (unblock function) des empfangenden Threads vom Thread (Ruby-Thread oder Timer-Thread) aufgerufen, der den Interrupt gesendet hat. Jeder Ruby-Thread hat eine ubf, die gesetzt wird, wenn er in eine blockierende Operation eintritt, und nach der Rückkehr davon zurückgesetzt wird. Standardmäßig sendet diese ubf-Funktion ein SIGVTALRM an den empfangenden Thread, um ihn aus dem Kernel zu entblockieren, damit er seine Interrupts prüfen kann. Es gibt andere ubfs, die nicht mit einem Systemaufruf verbunden sind, z. B. beim Aufruf von Ractor#join oder sleep. Alle ubfs werden mit gehaltener interrupt_lock aufgerufen, berücksichtigen Sie dies also, wenn Sie Sperren innerhalb von ubfs verwenden.

Denken Sie daran, dass ubfs vom Timer-Thread aufgerufen werden können, so dass Sie darin keinen ec annehmen können. Der ec (execution context) wird nur auf Ruby-Threads gesetzt.

Der Timer Thread

Der Timer-Thread hat mehrere Funktionen. Diese sind: