Prozesse
Executor
Eine Runtime führt ihre Prozesse auf ihren Scheduler-Workern aus (Worker, Ausführungsmodell). Beim Start wird der Aufruf der Einstiegsfunktion zum ersten Prozess, dem Hauptprozess, und der Executor läuft, bis dieser endet:
- Lauffähige Prozesse warten in der First-in-first-out-Run-Queue ihres Workers
(Worker). Ein Worker führt den ersten für eine Zeitscheibe von
4.000 Reduktionen aus (
CONTEXT_REDSvon OTP) und stellt ihn dann wieder ans Ende seiner Warteschlange, sofern er nicht geendet hat. - Jeder Funktionseintritt verbraucht eine Reduktion: Aufrufe, Endaufrufe, Fun- und dynamische Aufrufe sowie Builtins. Ist keine mehr übrig, findet der Eintritt nicht statt: Der Prozess merkt sich die Funktion, die er gerade betreten wollte, behält ihre Argumente in seinen Registern (sie bleiben Wurzeln der Speicherbereinigung) und kehrt zum Executor zurück, der ihn später fortsetzt, indem er den Eintritt wiederholt (Ausführungsmodell). Auch jeder Schleifenkopf einer Comprehension verbraucht eine, sodass auch eine Schleife ohne Aufrufe abgibt. Builtins, deren Arbeit mit einem Listen- oder Binary-Argument wächst, laufen in Portionen und geben zwischen ihnen ab (Portionen).
- Jeder Prozess besitzt seinen Heap und Stack. Argumente eines neuen Prozesses werden in seinen Heap kopiert (Kopieren zwischen Heaps).
- Ein Prozess endet, wenn sein erster Aufruf zurückkehrt oder eine Ausnahme
auslöst. Ein beendeter Prozess verlässt sofort die Prozesstabelle; seine Pid
bleibt ein gültiger Term, und
is_process_alive/1gibt für sie false zurück. Kontext, Heap und Stack werden freigegeben, sobald der Worker, der ihn beendet hat, den Executor-Mutex verlässt. - Ein Prozess, der in einem
receivewartet, ist in keiner Warteschlange; eine Sendung an ihn reiht ihn wieder ein (Receive). - Endet der Hauptprozess, endet das Programm mit seinem Ergebnis
(ausführbare Dateien): Noch eingereihte oder wartende
Prozesse werden freigegeben, ohne weiterzulaufen, so wie ein OTP-escript
anhält, wenn
main/1zurückkehrt. - Ein Exit-Signal, das den Hauptprozess beendet, beendet das Programm (Exit-Signale).
erlang:halt/0,1in einem beliebigen Prozess beendet das Programm mit dessen Status. Ein Runtime-Fehler in einem beliebigen Prozess (Speicher erschöpft, eine optionale Obergrenze überschritten, ein interner Fehler) beendet das Programm als Runtime-Fehler (Exit 70). Eine Erlang-Ausnahme beendet nur den Prozess, der sie ausgelöst hat (Exits).- Host-Aufrufe exportierter Funktionen (
CLAUSE_invoke_v1) führen ihre Funktion im aufrufenden Kontext bis zum Ende aus und setzen sie nach jeder Abgabe fort, ohne andere Prozesse laufen zu lassen; durchCLAUSE_main_v1gestartete Programme nutzen den Executor.
Worker
Der Executor führt Prozesse auf RuntimeOptions::schedulers Worker-Threads
aus: dem Thread, der das Programm gestartet hat, und je einem weiteren Thread
pro zusätzlichem Worker. Programme übernehmen die Anzahl aus --schedulers N
(1 bis 1.024, Runtime-Optionen); Standard ist
ein Worker pro logischem Prozessor, auf dem das Programm laufen darf (seiner
Affinitätsmaske, wie die Online-Scheduler von OTP), oder pro CPU aus
--scheduler-cpus.
- Jeder Worker hat seine eigene Run-Queue, wie jeder ERTS-Scheduler: Ein Prozess bleibt bei dem Worker, auf dem er zuletzt lief, sodass sein Heap in den Caches dieses Kerns bleibt. Ein gestarteter Prozess beginnt auf dem Worker seines Elternprozesses. Untätige Worker schlafen, bis ein Prozess für sie eingereiht wird oder es Arbeit zu stehlen gibt; der Timer-Thread reiht Prozesse ein, deren Empfangs-Timeout abläuft (Empfangs-Timeouts).
- Prozesse wandern wie bei ERTS, das seine Run-Queues periodisch ausgleicht: Alle 4 Zeitscheiben vergleicht ein Worker seine Warteschlange mit der seines Nachbarn und nimmt ihm einen Prozess ab, wenn der Nachbar mindestens zwei mehr hält. Ein Worker mit leerer Warteschlange stiehlt einen Prozess aus der längsten Warteschlange mit mindestens zwei Prozessen oder den einzigen Prozess eines schlafenden Workers, den er ausführt, bevor dieser Worker aufwacht.
- Aufwecken geschieht träge, so wie ERTS Prozesse, die miteinander sprechen, auf einem Scheduler hält: Ein laufender Prozess, der einen Prozess eines schlafenden Workers mit leerer Warteschlange weckt, reiht ihn auf seinem eigenen Worker ein, der ihn nimmt, wenn seine Zeitscheibe endet; ein anderer Worker wird nur geweckt, wenn eine Warteschlange dann mehr als einen Prozess hält. Die Timer- und I/O-Threads, Port-Aufgaben und Worker zwischen Zeitscheiben reihen einen Prozess für seinen eigenen Worker ein, oder für einen untätigen Worker, wenn sein eigener beschäftigt ist.
- Ein Executor-Mutex schützt die wartenden Prozesse, die Timer, die registrierten Namen, die Links und Monitore jedes Prozesses und jeden Prozess, der nicht läuft. Heap, Stack, Mailbox und Fehlerkanal eines laufenden Prozesses gehören allein seinem Worker; Erlang-Code läuft ohne die Sperre. Die Run-Queues und der Ruhezustand der Worker haben eine eigene Sperre, die nach dem Executor-Mutex genommen wird; untätige Worker schlafen darauf (Prozesserzeugung und -abbau).
- Ein Spawn baut den Kontext des neuen Prozesses auf und kopiert seinen Aufruf
ohne jede Sperre hinein. Einer, der weder verlinkt noch überwacht, trägt ihn
dann nur unter den jeweiligen eigenen Sperren in die Prozesstabelle und die
Warteschlange ein, ohne je den Executor-Mutex zu nehmen;
spawn_link,spawn_monitorund die überwachenden Formen vonspawn_opt/spawn_requesttragen ihn unter dem Executor-Mutex ein, verlinken oder überwachen ihn und reihen ihn ein. Ein Worker gibt die Kontexte beendeter Prozesse direkt nach dem Verlassen des Mutex für seine nächste Zeitscheibe frei. - Ein Prozess kostet über seine Lebensdauer sechs Allokationen (Kosten der Prozesserzeugung): seinen Kontext, die Runtime-Bindungen des Kontexts mit seinem Scheduling-Zustand, seinen Heap-Speicher und ersten Heap-Block, seinen Nachrichteneingang und sein Lebensdauer-Token. Die ersten 64 Stack-Wörter und vier Mailbox-Listenknoten liegen im Kontext, die Monitor-maps werden beim ersten Monitor angelegt, und die Registrys der Runtime (Kontexte, Freigabe persistenter Terme) halten keinen Knoten pro Prozess.
- Links, Monitore und Namen ändern sich sofort unter der Sperre, auch für einen
Prozess, der anderswo läuft. Eine Nachricht wartet in einem Puffer unter
den Eingängen des Empfängers, bis dieser sie übernimmt
(Eingangspuffer). Ein Exit-Signal von
exit/2oderexit_signal/2an einen solchen Prozess tut nichts, und sein Prozess beendet seine Zeitscheibe (builtins::Blocked); er wartet, bis die Zeitscheibe des Ziels endet, läuft dann als Erster und wiederholt das Builtin mit denselben Argumenten. Das Ziel wird bis dahin festgehalten, sodass das wiederholte Builtin es nicht erneut laufend vorfinden kann. Ein Prozess, der mit Links oder Monitoren endet, deren Prozesse anderswo laufen, wird ebenso abgeschlossen, sobald deren Zeitscheiben enden. - Signale behalten also ihre Reihenfolge: Eine Nachricht, die vor einem
blockierten Exit-Signal desselben Senders gepuffert wurde, erreicht den
Empfänger zuerst, und
is_process_alive/1nachexit(Pid, kill)ist false, wie OTP es für Signale vom Aufrufer zusagt. - Ein Spawn verlinkt oder überwacht den neuen Prozess (
spawn_link,spawn_monitor), bevor irgendein Worker ihn ausführen kann. Ein gestarteter Prozess kann laufen, bevor sein Elternprozess weitermacht, daher kann ein Programm, das nachspawn/1einen kurzlebigen Prozess überwacht oder sich mit ihm verlinkt,noprocsehen, wie in einem OTP mit mehreren Schedulern. - Das Ende des Hauptprozesses, ein Halt oder ein Runtime-Fehler stoppt das Programm, sobald jeder Worker seine aktuelle Zeitscheibe beendet hat; dann werden die anderen Prozesse freigegeben.
- Gemeinsame Runtime-Dienste sind synchronisiert (Threads): Atome, der Code-Server, Pid- und Port-Nummern und das Speicherkonto.
- Die Ausgaben verschiedener Prozesse verschränken sich in der Reihenfolge, in
der ihre Schreibvorgänge geschehen. Jeder Aufruf von
io:formatunderlang:displayschreibt seinen Text sofort.
Aufwecken und Herunterfahren brauchen keinen weiteren Mechanismus, weil jede Änderung am Scheduling-Zustand eines Prozesses unter dem Executor-Mutex geschieht:
- Eine Nachricht,
'EXIT'oder'DOWN'für einen wartenden Prozess reiht ihn im selben kritischen Abschnitt ein, der sie zustellt, und weckt einen Worker wie oben beschrieben. Ein Prozess, der sich noch in der Zeitscheibe befindet, in der er zu warten begann, kann dann keine Nachricht erhalten (seine Sender warten auf das Ende der Zeitscheibe), daher findet die Zustellung ihn immer geparkt vor. Auch eine eingestellte Nachricht (Eingangspuffer) geht nicht verloren: Das Parken prüft die Eingänge unter ihrer Sperre, und ein Einstellen, das den Empfänger geparkt vorfindet, weckt ihn unter dem Executor-Mutex. - Ein Worker, der keinen lauffähigen Prozess findet, schläft, bis ein anderer Worker, eine Sendung, ein Spawn oder der Timer-Thread einen einreiht oder das Programm endet; er prüft die Warteschlangen und schläft unter der Sperre der Run-Queues, die jedes Einreihen nimmt. Worker lesen die Uhr nie für Timer; das Parken eines Prozesses mit einem Timeout, der früher als jeder andere ist, weckt nur den Timer-Thread.
- Eine Nachricht und ein ablaufender Timeout desselben receive können konkurrieren: Was zuerst kommt, reiht den Prozess ein und hebt das andere auf, und das receive nimmt eine Nachricht, die vor seiner Fortsetzung angekommen ist, wie bei OTP.
- Ein Exit-Signal an einen wartenden, eingereihten, festgehaltenen oder blockierten Prozess holt ihn aus seinem jeweiligen Zustand, bevor er abgeschlossen wird.
- Endet das Programm, beendet jeder Worker seine aktuelle Zeitscheibe und
stoppt;
run()kehrt zurück, nachdem es sie gejoint hat, und jeder andere vom Executor gestartete Prozess wird freigegeben, sodass die Runtime ohne verbleibenden Kontext herunterfährt. - Das OTP-Golden
executables_wakeupstestet dies unter Last mit 1, 2, 4 und allen Workern: Empfänger, deren Timeouts von 0–2 ms mit den Nachrichten ihrer Sender konkurrieren, eine Kette von 16 verlinkten, rotierenden Prozessen, die ein einziges Exit-Signal beendet, wobei jedes Glied überwacht wird, überwachte Prozesse, die enden, während ihr Beobachter aufwacht, ein Programm, das endet, während Prozesse rotieren, warten und einander fluten, und ein Halt in einem anderen Prozess.
CPU-Bindung
Worker sind standardmäßig ungebunden: Das Betriebssystem platziert sie auf
beliebigen CPUs der Affinitätsmaske des Programms, wie beim Standard +sbt u
von OTP. --scheduler-cpus SET
(Runtime-Optionen) bindet Worker N an die
N-te CPU der Menge, bei mehr Workern reihum, wie +sbt/+sct von OTP; die
Timer- und I/O-Threads bleiben ungebunden.
performanceist die schnellste Kernklasse: unter Windows die Kerne mit der höchstenEfficiencyClass, unter Linux/sys/devices/cpu_core/cpus(Intel Hybrid) oder die CPUs mit der größtencpu_capacity; bei einer CPU mit gleichartigen Kernen jede CPU.allumfasst jede CPU der Affinitätsmaske. Eine Liste (0-15,2,4,6, CPUs bis 1023) darf nur CPUs der Affinitätsmaske nennen:--scheduler-cpus: CPU N is not available to the program, Exit 70.- Windows nummeriert die CPUs der Prozessorgruppe des Prozesses (höchstens
64);
performanceliest Gruppe 0. - macOS hat keine Thread-Affinität: Die Option wird akzeptiert,
performancezählt für die Standardanzahl der Worker die erste Leistungsstufe (hw.perflevel0.logicalcpu), und die Bindung hebt jeden Worker lediglich in die QoS-KlasseUSER_INTERACTIVE, die macOS bevorzugt auf den P-Kernen bedient.
Empfangs-Timeouts
Timer liegen in einem hierarchischen gehashten Timer-Rad (timer wheel), wie
ERTS sie führt (scheduler/timer_wheel):
- Ticks sind Millisekunden seit dem Start des Executors. Sechs Ebenen mit je 64 Slots decken 2^36 Ticks ab, mehr als der längste Timeout (4.294.967.295 ms). Ein Timer liegt im Slot der höchsten 6-Bit-Ziffer, in der sich sein Tick vom aktuellen Tick des Rads unterscheidet; belegte Slots sind Bitmaps pro Ebene.
- Das Scharfschalten (ein wartender Prozess wird geparkt) und das Aufheben (eine Nachricht, ein Exit-Signal oder das Ende des Prozesses kommt zuerst) hängen einen intrusiven Listenknoten aus oder ein, der beim Scheduling-Zustand des Prozesses liegt: konstante Zeit. Eine Frist wird zum Tick an oder nach ihr, sodass kein Timeout zu früh auslöst.
- Ein Timer-Thread pro Lauf schläft bis zum nächsten belegten Slot (dem ersten belegten Slot nach dem aktuellen auf irgendeiner Ebene), liest beim Aufwachen einmal die Uhr und rückt das Rad bis zu diesem Tick vor: Slots höherer Ebenen, die an ihrem Anfang erreicht werden, verschieben ihre Timer nach unten, und der Slot der Ebene 0 löst aus. Nur Slots, deren Zeit gekommen ist, werden befragt; wartende Prozesse werden nie durchsucht. Abgelaufene Prozesse werden in Tick-Reihenfolge eingereiht.
- Das Rad wird von den Workern unter dem Executor-Mutex geteilt.
after 0wartet nie, undinfinityschaltet nie einen Timer scharf. --profilemeldet die Zeitscheiben und die Uhrablesungen des Timer-Threads (Profiling). Timer lösen nur so genau aus wie die zeitgesteuerten Wartevorgänge des Hosts (unter Windows die Standard-Timerauflösung).runtime_timer_wheelschaltet 20.000 Timer über alle Ebenen scharf, hebt 90 % auf und rückt in ungleichmäßigen Schritten vor: Jeder verbleibende Timer löst genau einmal aus, beim ersten Vorrücken, das seinen Tick erreicht, in Tick-Reihenfolge; kein aufgehobener löst aus.
Exits
Ein Prozess außer dem Hauptprozess endet mit einem Exit-Grund, wie in OTP
(detail::exit_reason, process/exits). Exit-Signale tragen ihn zu
verlinkten Prozessen (Links) und 'DOWN'-Nachrichten zu
überwachenden (Monitore); Fehlerberichte zeigen ihn.
| Wie der Prozess endet | Exit-Grund | Fehlerbericht |
|---|---|---|
| Sein erster Aufruf kehrt zurück | normal | Keiner |
exit(Reason) (auch normal, kill) | Reason | Keiner |
Ein Fehler (error/1,2,3, badarith, {badmatch, V}, undef, ...) | {Reason, Stack} | Ja |
Ein nicht abgefangenes throw(Value) | {{nocatch, Value}, Stack} | Ja |
| Ein Exit-Signal beendet ihn (Exit-Signale) | Der Grund des Signals, killed für exit(Pid, kill) | Keiner |
Ein Fehlerbericht wird auf stderr geschrieben, wenn der Prozess endet, nach dem Leeren der Standardausgabe, im Format des Standard-Logger-Handlers von OTP:
=ERROR REPORT==== 8-Oct-2026::03:42:15.983000 ===
Error in process <0.8.0> with exit value:
{boom,[{crash_reports,'-main/1-fun-6-',0,[]}]}
Der Kopf enthält die Ortszeit; der Grund wird so gesetzt, wie ~p es tut. Der
Hauptprozess schreibt keinen: Seine nicht abgefangene Ausnahme ist die des
Programms (ausführbare Dateien).
Nachrichten
Dest ! Msg und erlang:send(Dest, Msg) werten zuerst Dest aus und geben
Msg zurück.
Dest | Wirkung |
|---|---|
| Eine Pid eines lebenden Prozesses | Msg wird flach in den Heap des Empfängers kopiert (Kopieren zwischen Heaps), wie OTP Nachrichten kopiert, eine große in einen Block, den der Empfänger übernimmt (Nachrichtenübergabe), und an seinen Signal-Eingang angehängt; bei einem Empfänger, der auf einem anderen Worker läuft, in einen Puffer unter seinen Eingängen, den er beim nächsten Nachsehen übernimmt (Eingangspuffer) |
| Eine Pid eines beendeten Prozesses | Nichts; die Sendung gelingt |
| Ein registrierter Name | Wie für seine Pid; badarg, wenn kein lebender Prozess den Namen hat |
{Name, nonode@nohost} aus zwei Atomen | Wie für die als Name registrierte Pid; nichts, wenn es keine gibt |
{Name, Node} für jeden anderen Knoten | Nichts (es gibt keine anderen Knoten) |
| Eine Referenz | Wie für den Besitzer eines aktiven Alias; sonst nichts |
| Alles andere | badarg |
- Nachrichten eines Senders kommen in der Reihenfolge an, in der er sie gesendet hat; eine Sendung an sich selbst ist eine Nachricht wie jede andere.
- Jede Nachricht ist eine Wurzel der Speicherbereinigung ihres Empfängers, bis ein receive sie nimmt (Runtime-Heap).
- Die Mailbox des Empfängers (
Mailbox) führt einen Signal-Eingang, an den Sendungen anhängen, und eine Nachrichtenwarteschlange, die receive ab einer gespeicherten Position durchsucht: Angekommene Nachrichten wandern hinter die Warteschlange, wenn ein receive sie untersucht. Beide Listen verwenden bis zu 64 freie Knoten der Mailbox wieder, sodass Senden und Empfangen einer Nachricht keinen Knoten vom C++-Heap allozieren. - Verweigert der Heap des Empfängers die Kopie (eine optionale Obergrenze wie
--max-heapoder kein Speicher mehr im Host), wird nichts zugestellt, und der sendende Prozess scheitert als Runtime-Fehler (Exit 70), wie jeder Prozess, der eine Obergrenze überschreitet; ein Empfänger, der eine Nachricht aus seinen Eingängen verweigert, scheitert selbst.
Eingangspuffer
Eine Sendung an einen Prozess, der auf einem anderen Worker läuft, nimmt überhaupt keine Sperre (Prozesserzeugung und -abbau):
- Die Prozesstabelle der Runtime (
ProcessTable) ist ein Array atomarer Plätze mit offener Adressierung, das ein Senden auf einem Worker ohne jede Sperre liest: Pid-Nummern werden nie wiederverwendet, daher bleibt ein gelöschter Platz ein Löschmarker, bis ein Einfügen das Array neu aufbaut. Einfügen und Löschen nehmen eine Schreibsperre und halten außerdem die Executor-Sperre, unter der Suchzugriffe nichts Weiteres benötigen. - Jeder Prozess hat seine Eingänge (
Arrivals): eine sperrfreie Liste, auf die Sender Nachrichten legen, mit einer Kopie des Laufzustands des Prozesses im Executor. Der Sender schlägt die Eingänge des Empfängers nach und liest diesen Zustand; ein Sender, dessen letzte Sendung an denselben Empfänger ihn nicht laufend vorfand, überspringt den Suchzugriff. - Der Sender durchläuft die Nachricht einmal, vor jeder Sperre
(ein Nachrichtendurchlauf). Für einen
laufenden Empfänger kopiert er die Nachricht direkt hinter einen
Listenknoten, in einer Allokation (eine große Nachricht oder eine mit
Off-Heap-binaries in einen
MessageBuffer), und stellt sie ein; der Empfänger kopiert diese Wörter später in seinen Heap und verschiebt ihre Zeiger. Jeder andere Empfänger erhält die Nachricht wie bisher unter der Executor-Sperre: in seinen Heap kopiert oder hinter seine Eingänge gestellt, wenn er inzwischen eine Zeitscheibe begonnen hat oder noch Eingänge hat, sodass Nachrichten eines Senders einander nie überholen und der Sender nie einen Rückstau unter der Sperre kopiert. Beide Ergebnisse eines Wettlaufs sind korrekt. - Der Empfänger übernimmt seine Eingänge in seinen Heap und Signal-Eingang,
die ältesten zuerst: jeweils 64, wenn sein receive jede Nachricht untersucht
hat und warten würde, sodass eine Flut den Heap so schnell erreicht, wie das
receive sie verbraucht (OTP hält solche Nachrichten in Heap-Fragmenten, bis
sie gebraucht werden); alle, wenn er in den Ruhezustand geht, und bevor ihm
unter der Executor-Sperre irgendetwas anderes zugestellt wird (eine
Nachricht,
'EXIT','DOWN', Port- oder Alias-Nachricht), sodass die Nachrichten jedes Senders ihre Reihenfolge relativ zu seinen anderen Signalen behalten.process_infoübernimmt sie vor dem Zählen oder Auflisten von Nachrichten. Timer-Nachrichten und andere Nachrichten für einen laufenden Prozess warten ebenfalls unter den Eingängen. - Ein Prozess parkt unter der Executor-Sperre erst nach einer sequenziell konsistenten Prüfung, dass kein Eingang wartet; ein Einstellen, das ihn geparkt vorfindet, nimmt die Executor-Sperre und weckt ihn.
- Die Eingänge eines beendeten Prozesses und ersetzte Tabellen-Arrays werden erst freigegeben, sobald jeder Worker nach ihrer Ausmusterung eine Zeitscheibengrenze passiert hat oder auf Arbeit wartet (Freigabe nach Ruhepunkten): Ein Worker vermerkt die Freigabeepoche an jeder Zeitscheibengrenze, und eine Sendung hält das Gefundene nur innerhalb ihrer Zeitscheibe.
- Eine Sendung an sich selbst geht direkt in den eigenen Signal-Eingang des Senders.
Receive
receive wählt wie case unter seinen Klauseln aus, über die Nachrichten der
Mailbox:
- Die Suche in der Mailbox beginnt bei der ältesten Nachricht. Jede Nachricht
wird der Reihe nach gegen die Klauseln gematcht (Muster und Guards, die
Bindungen von vor dem
receivelesen dürfen); die erste Nachricht, die irgendeine Klausel matcht, wird entfernt, und der Rumpf dieser Klausel läuft. Nachrichten, die keine Klausel matcht, bleiben in ihrer Reihenfolge in der Mailbox; das nächste receive beginnt wieder bei der ältesten Nachricht. - Wurde jede Nachricht untersucht, wartet der Prozess
(
CLAUSE_wait_frame_v1, ein Builtin, das wie ein Aufruf betreten wird): Er verlässt die Run-Queue, bis eine Sendung ihm eine Nachricht zustellt, dann geht die Suche mit den angekommenen Nachrichten weiter. Wartende Prozesse behalten ihre Frames und Nachrichten als Wurzeln der Speicherbereinigung und bereinigen bei der Fortsetzung (wartende Prozesse). - Namen, die in jeder Klausel gebunden werden, und im
after-Rumpf, falls es einen gibt, werden nach demreceiveexportiert, wie beicase; ein Aufruf in Endposition eines Klausel- oderafter-Rumpfs ist ein Endaufruf, sodass eine Server-Schleife mit konstantem Stack läuft. after T -> Body:Twird zuerst ausgewertet, vor der Suche. Würde das receive warten, mussTinfinityoder eine Ganzzahl in 0..4294967295 (Millisekunden) sein, sonsterror:timeout_value, wonach das nächste receive ab der ältesten Nachricht sucht; eine Nachricht, die sofort matcht, prüft ihn nie, wie in OTP.after 0führtBodyaus, sobald jede Nachricht untersucht wurde. Ein endlicher Timeout beginnt, wenn das receive zum ersten Mal wartet, und wird durch Nachrichten, die keine Klausel matchen, nicht neu gestartet; läuft er ab, läuftBodymit den Bindungen von vor dem receive, und das nächste receive sucht ab der ältesten Nachricht. Eine Nachricht, die vor Ablauf des Timeouts ankommt, wird genommen. Ein receive nur mitafterist ein Schlafen (das Idiom vontimer:sleep/1).- Erzeugter Code: Ein Schleifenkopf schaut auf die nächste nicht untersuchte
Nachricht (
CLAUSE_receive_v1peek, in einen Wurzel-Slot), die Klauselauswahl nimmt eine gematchte Nachricht (take) vor ihrem Rumpf oder überspringt eine nicht gematchte (skip) und läuft weiter; ist keine Nachricht mehr übrig, betritt die Schleife das Warten mit dem Timeout, dastrue(erneut suchen) oderfalse(Timeout abgelaufen:restart, dann derafter-Rumpf) antwortet. - Der Executor führt einen Timer pro wartendem Prozess mit endlichem Timeout in einem Timer-Rad (Empfangs-Timeouts): Ein abgelaufener reiht den Prozess wieder ein. Timeouts werden auf einer monotonen Uhr in Millisekunden gemessen und lösen nie zu früh aus.
- Wartet jeder Prozess ohne Timeout auf eine Nachricht, die nichts senden kann,
wartet das Programm ewig, wie bei OTP. Ein Host-Aufruf (
CLAUSE_invoke_v1), der warten würde, scheitert stattdessen mitbusy: Während ihm läuft kein anderer Prozess.
Links
Ein Link verbindet zwei Prozesse in beide Richtungen (Signals in jedem
Kontext: die verlinkten Pids in Link-Reihenfolge und das Flag trap_exit).
link(Pid)verlinkt den Aufrufer mit einem lebenden Prozess und gibttruezurück; ein Link mit sich selbst oder ein erneuter Link tut nichts. Für einen beendeten Prozess löst eserror:noprocaus oder gibt, wenn der Aufrufer Exits abfängt,truezurück und sendet dem Aufrufer{'EXIT', Pid, noproc}, wie es das lokalelink/1von OTP tut.unlink(Pid)entfernt den Link auf beiden Seiten; der Link hat nach der Rückkehr keine Wirkung mehr. Auchtrue, wenn es keinen Link gab.spawn_link/1,3verlinken den neuen Prozess mit dem Aufrufer, bevor er läuft.- Endet ein Prozess, erhält jeder verlinkte Prozess ein Exit-Signal mit seinem Exit-Grund (Exits), und der Link ist weg. Verlinkte Prozesse werden in der Reihenfolge signalisiert, in der die Links entstanden.
Monitore
Ein Monitor ist einseitig: Der überwachende Prozess hält ihn (per Referenz, in
Signals), und der überwachte Prozess bewahrt die Referenz und die
überwachende Pid, damit er die Nachricht senden kann, wenn er endet.
monitor(process, Pid)gibt eine neue Referenz zurück. Endet der Prozess, erhält der Aufrufer{'DOWN', Ref, process, Pid, Reason}mit dessen Exit-Grund (Exits); für einen bereits beendeten Prozess erhält er die Nachricht sofort mit Grundnoproc. Sich selbst zu überwachen erzeugt nichts. Jeder Aufruf erzeugt einen eigenen Monitor mit eigener Nachricht; die Nachrichten eines Prozesses gehen in der Reihenfolge hinaus, in der die Monitore entstanden.demonitor(Ref)stoppt den Monitor: Danach kommt kein'DOWN'von ihm an. Es gibttruezurück, auch für eine Referenz, die kein aktiver Monitor des Aufrufers ist.demonitor(Ref, Options):infogibt zurück, ob der Monitor noch aktiv war;flushentfernt die älteste Nachricht{_, Ref, _, _, _}, wenn er es nicht war (sein'DOWN'ist schon in der Mailbox), wie OTP es tut.spawn_monitor/1,3geben{Pid, Ref}zurück, den Monitor, der vor dem Lauf des neuen Prozesses erzeugt wurde.- Ein Prozess, der endet, verwirft die Monitore, die er hält.
monitor(process, Name)undmonitor(process, {Name, nonode@nohost})überwachen den alsNameregistrierten Prozess (sofortnoproc, wenn es keinen gibt); sein'DOWN'nennt{Name, nonode@nohost}statt der Pid.{Name, Node}für einen anderen Knoten istbadarg.monitor(port, Port | Name)überwacht einen Port (Ports):{'DOWN', Ref, port, Port, Reason}, wenn er sich schließt. Eine Pid fürportoder ein Port fürprocessistbadarg.
Registrierte Namen
Der Executor führt eine Tabelle von Namen (Atomen) zu Pids, und jeder Prozess
seinen eigenen Namen (Signals::name).
register(Name, Pid)benennt einen lebenden Prozess:badargfürundefined, einen vergebenen Namen, einen Prozess, der schon einen Namen hat, einen beendeten Prozess oder einPid, das keine Pid ist. Ein Prozess darf sich selbst registrieren.unregister(Name)gibt den Namen frei (badarg, wenn kein Prozess ihn hat);whereis(Name)ist die Pid oderundefined;registered()listet die Namen in Reihenfolge der Atomtabelle (die Reihenfolge von OTP ist ebenfalls unspezifiziert).- Der Name eines Prozesses wird freigegeben, wenn er endet, bevor seine Links
und Monitore signalisiert werden, sodass ein Empfänger von
'DOWN'oder'EXIT'den Namen erneut registrieren kann, wie in OTP.
Prozess-Dictionary
put/2, get/0,1, get_keys/0,1 und erase/0,1 verhalten sich wie bei OTP:
put/2 und erase/1 geben den vorherigen Wert oder undefined zurück,
erase/0 gibt jedes {Key, Value} zurück und leert das Dictionary, Schlüssel
werden mit exakter Gleichheit gematcht (1 und 1.0 sind zwei Schlüssel).
- Jeder Prozess hat sein eigenes Dictionary; ein gestarteter Prozess beginnt leer.
- Einträge sind Wörter des Prozess-Heaps, die in
ProcessDictionary(process_dictionary.hpp) gehalten und als Wurzeln der Speicherbereinigung besucht werden (Wurzeln); sie werden mit dem Prozess freigegeben. - Die Reihenfolge von
get/0undget_keys/0,1ist unspezifiziert, wie in OTP. Der Lookup durchsucht die Einträge linear; Immediate-Schlüssel werden wortweise verglichen.put/2,get/1underase/1nehmen und liefern die Eintragswörter so, wie sie sind, ohne einen Term zuzulassen.
Timer
start_timer/3,4 und send_after/3,4 (builtins/erlang/timers.cpp,
scheduler/timers.cpp) schalten einen Timer im Rad des Executors scharf (dem,
das die Empfangs-Timeouts verwenden), der beim Auslösen
Msg oder {timeout, TimerRef, Msg} an eine Pid oder einen registrierten
Namen sendet.
- Die Nachricht wird beim Start des Timers in einen
MessageBufferkopiert (Nachrichtenpuffer), einen eigenen Heap-Block des Timers, und beim Auslösen erneut in den Heap des Empfängers kopiert, wie ein Senden kopiert: Jeder Term, lokale Funs eingeschlossen, reist unverändert. - Ein Empfänger, der auf einem Worker läuft, erhält die Nachricht, wenn seine Zeitscheibe endet. Ein registrierter Name wird beim Auslösen des Timers aufgelöst; ein Name, den niemand hält, oder die Pid eines beendeten Prozesses verwirft die Nachricht. An eine Pid gesendete Timer werden freigegeben, wenn dieser Prozess endet.
- Die Zeit ist relativ in Millisekunden in 0..2^49-1 oder mit
{abs, true}ein absoluteserlang:monotonic_time(millisecond); alles andere istbadarg. Eine Frist jenseits der Reichweite des Rads wird erneut scharfgeschaltet, wenn dieser Slot an der Reihe ist. cancel_timer/1,2undread_timer/1,2geben die verbleibenden Millisekunden oderfalsezurück;{async, true}gibtokzurück und sendet{cancel_timer, Ref, Left}({read_timer, ...}),{info, false}gibtokohne Ergebnis zurück.
Introspektion
process_info/1,2 (builtins/erlang/introspection.cpp,
scheduler/introspection.cpp) liest einen anderen Prozess unter der Sperre des
Executors und kopiert seine Terme (Nachrichten, Dictionary) in den Heap des
Aufrufers; ein Prozess, der auf einem anderen Worker läuft, wird gelesen,
sobald seine Zeitscheibe endet. Eine Pid ohne lebenden Prozess ergibt
undefined.
- Elemente:
registered_name([]allein, wenn nicht registriert),status(running,runnable,waiting),message_queue_len,messages,links,monitors,monitored_by,dictionary,trap_exit,group_leader,heap_size,total_heap_size,stack_size,memory(Wörter des Heaps, seiner Fragmente und Off-Heap-Binaries sowie des Stacks),reductions(0),priority(normal),error_handler,garbage_collection.process_info/1listet sie vonstatusbisgarbage_collection. - OTP-Elemente, die Clause nicht führt (
current_function,initial_call,current_stacktrace, ...), lösenerror:{notsup, Item}aus; unbekannte Elemente sindbadarg. processes/0listet lebende Pids in Erzeugungsreihenfolge. Ein gestarteter Prozess erbt den Group Leader seines Elternprozesses; der erste Prozess führt sich selbst.group_leader/2mit der Pid eines nicht lebenden Prozesses istbadarg.garbage_collect/0,1,2fordert eine Bereinigung am nächsten Safepoint des Ziels an und gibttruezurück (falseohne lebenden Prozess); mit{async, RequestId}gibt esasynczurück und sendet{garbage_collect, RequestId, Result}.yield/0beendet die Zeitscheibe beim nächsten Funktionseintritt.
Spawn-Optionen
Quellen: builtins/erlang/spawning.cpp, scheduler/aliases.cpp.
spawn_opt/2..5nehmenlink,monitor,{monitor, MonitorOptions}und die Tuning-Optionen{priority, P},{fullsweep_after, N},{min_heap_size, N},{min_bin_vheap_size, N},{max_heap_size, S},{message_queue_data, M},{async_dist, B};{min_heap_size, N}setzt den Mindest-Heap des Kindprozesses, die anderen Tuning-Optionen werden geprüft und akzeptiert, nicht angewendet. Ein anderer Knoten alsnonode@nohostistbadarg.spawn_request/1..5startet sofort und sendet{Tag, ReqId, ok, Pid}({Tag, ReqId, error, Reason}bei Fehlschlag,badoptfür eine ungültige Option), wie{reply_tag, Tag}und{reply, yes | no | error_only | success_only}es verlangen; mitmonitorist die Anfrage-ID die Monitor-Referenz.spawn_request_abandon/1gibtfalsezurück: Die Antwort ist bereits gesendet.hibernate(M, F, A)wartet, bis die Mailbox eine Nachricht hält, und ruft dannM:F(A...)anstelle des Aufrufs auf; der Stack bleibt erhalten, sodass das Ergebnis zum Aufrufer zurückkehrt.
Aliase
Ein Alias ist eine Referenz, an die Nachrichten gesendet werden können
(Alias ! Msg): alias/0,1 (explicit_unalias oder reply für eine
Nachricht) oder ein Monitor, der mit
{alias, explicit_unalias | demonitor | reply_demonitor} von monitor/3
erzeugt wurde (demonitor endet mit dem Monitor, reply_demonitor beendet bei
seiner ersten Nachricht auch den Monitor). unalias/1 beendet einen Alias des
Aufrufers. Eine Sendung an eine Referenz, die kein aktiver Alias ist oder deren
Besitzer geendet hat, wird verworfen. Ein Antwort-Alias wird erst verbraucht,
wenn seine Nachricht zugestellt wird, nach jedem Warten auf einen beschäftigten
Besitzer. {tag, Tag} von monitor/3 ersetzt 'DOWN' in der
Monitor-Nachricht; Port-Monitore nehmen keine Optionen.
Knoten
Ein Clause-Programm ist ein einzelner, nicht verteilter Knoten,
nonode@nohost, und antwortet wie ein solcher OTP-Knoten
(builtins/erlang/node.cpp): nodes/0 ist [], nodes(this | known)
[nonode@nohost], andere Typen []; nodes/2 liefert
{nonode@nohost, #{connection_id => undefined, node_type => this}} für die abgefragten Schlüssel. is_alive/0 ist false,
monitor_node/2,3 lösen error:notalive aus, und disconnect_node/1 gibt
ignored zurück.
Exit-Signale
Jedes Exit-Signal kommt von einem laufenden Prozess (exit/2,
exit_signal/2, link/1) oder vom Ende eines Prozesses. Der Executor
verarbeitet es sofort, wenn sein Ziel nicht auf einem anderen Worker läuft,
sonst sobald die Zeitscheibe des Ziels geendet hat (Worker,
scheduler/signals): Ein beendetes Ziel verlässt die Run-Queue oder sein
Warten und wird abgeschlossen (seine Links signalisiert, sein Fehlerbericht
geschrieben, sein Kontext freigegeben), bevor das sendende Builtin zurückkehrt.
Eine lange verlinkte Kette endet Prozess für Prozess ohne Rekursion.
| Signal an einem Prozess | Fängt Exits nicht ab | Fängt Exits ab |
|---|---|---|
exit(Pid, kill), exit_signal(Pid, kill) | Endet mit Grund killed | Endet mit Grund killed |
Grund normal (von einem Link oder an einen anderen Prozess gesendet) | Nichts | Nachricht {'EXIT', From, normal} |
exit(self(), normal) | Endet mit Grund normal (Eigenheit von OTP) | Nachricht {'EXIT', Self, normal} |
exit_signal(self(), normal) | Nichts | Nachricht {'EXIT', Self, normal} |
Jeder andere Grund, einschließlich kill von einem Link | Endet mit diesem Grund | Nachricht {'EXIT', From, Reason} |
process_flag(trap_exit, Bool)setzt das Abfangen und gibt die vorherige Einstellung zurück (anfangsfalse);process_flag(min_heap_size, N)setzt den Mindest-Heap ab der nächsten Bereinigung und gibt den vorherigen zurück.- Ein Prozess, der durch ein Signal endet, das er sich selbst gesendet hat
(
exit(self(), kill)), wickelt sich an jedemcatch, jedemtry-Handler und jedemafter-Rumpf vorbei ab: Das Signal ist keine Ausnahme (CallError::exitedim Fehlerkanal). exit/2undexit_signal/2gebentruezurück; an einen beendeten Prozess tun sie nichts. Ein Referenz-Ziel tut ebenfalls nichts, selbst ein aktiver Alias.- Ein Exit-Signal, das den Hauptprozess beendet, beendet das Programm wie ein
nicht abgefangenes
exit(Exit-Status): Grundnormalendet mit 0. - Verweigert der Heap des Empfängers einen Exit-Grund oder eine
'EXIT'-Nachricht, scheitert der Empfänger als Runtime-Fehler (Exit 70).
Ports
Ports sind in Ports spezifiziert: Ein Port ist mit seinem Öffner verlinkt, nimmt wie ein Prozess an Links, Monitoren, Exit-Signalen und registrierten Namen teil und spricht mit seinem verbundenen Prozess über Nachrichten.
Prozesse anhalten
erlang:suspend_process/1,2 und erlang:resume_process/1
(scheduler/suspension.cpp): Jedes Anhalten hält das Ziel einmal im Executor
fest, sodass ein angehaltener Prozess erst wieder läuft, wenn jedes Anhalten
jedes anhaltenden Prozesses aufgehoben wurde; Nachrichten und Signale
erreichen ihn weiterhin (ein Exit-Signal beendet ihn weiterhin), und
process_info(Pid, status) meldet suspended. Das Ende eines anhaltenden
Prozesses hebt dessen Anhalten auf. Ein synchrones Anhalten eines Prozesses,
der auf einem anderen Worker läuft, wartet auf das Ende seiner Zeitscheibe;
asynchronous kehrt sofort zurück, {asynchronous, Tag} sendet dem Aufrufer
zusätzlich {Tag, suspended}, und unless_suspending antwortet false, wenn
der Aufrufer den Prozess bereits anhält. Das Anhalten oder Fortsetzen des
Aufrufers selbst, eines beendeten Prozesses, einer Nicht-Pid oder ungültige
Optionen sind badarg, ebenso das Fortsetzen eines Prozesses, den der
Aufrufer nicht anhält.
Builtins
| Builtin | Verhalten |
|---|---|
spawn(Fun) | badarg, außer Fun ist eine Fun; sonst ruft ein neuer Prozess Fun() auf und löst in diesem Prozess bei anderer Stelligkeit {badarity, {Fun, []}} aus |
spawn(M, F, Args) | badarg, außer M und F sind Atome und Args eine echte Liste; sonst ruft ein neuer Prozess M:F(Args...) auf und löst in diesem Prozess undef aus, wenn kein Modul sie exportiert und kein Builtin diesen Namen hat |
spawn_link(Fun), spawn_link(M, F, Args) | Wie spawn, und der neue Prozess wird mit dem Aufrufer verlinkt (Links) |
is_process_alive(Pid) | badarg, außer Pid ist eine Pid; wahr, solange ihr Prozess nicht geendet hat |
spawn_monitor(Fun), spawn_monitor(M, F, Args) | Wie spawn, gibt {Pid, Ref} eines neuen Monitors zurück |
link(Pid), unlink(Pid) | badarg, außer Pid ist eine Pid (Links) |
monitor(process, Item) | badarg für einen anderen Typ oder ein anderes Element; eine Referenz (Monitore); Item ist eine Pid oder ein registrierter Name |
register(Name, Pid), unregister(Name), whereis(Name), registered() | Siehe registrierte Namen; Name muss ein Atom sein |
demonitor(Ref), demonitor(Ref, Options) | badarg, außer Ref ist eine Referenz und Options eine echte Liste aus flush und info |
exit(Dest, Reason), exit_signal(Dest, Reason) | badarg, außer Dest ist eine Pid oder Referenz; true nach dem Exit-Signal |
process_flag(trap_exit, Bool), process_flag(min_heap_size, N), process_flag(save_calls, N) | Die vorherige Einstellung; badarg für ein anderes Flag, einen Nicht-Boolean oder eine negative oder nicht ganzzahlige Größe |
erlang:send(Dest, Msg), Dest ! Msg | Msg, nach dem Senden (Nachrichten); send/2 wird nicht automatisch importiert |
spawn(Node, ...), spawn_link(Node, ...), spawn_monitor(Node, ...) (/2, /4) | Dieser Knoten (nonode@nohost): wie ohne Node. Anderer Knoten: die nutzlose Pid eines nicht lebenden Knotens von OTP, ein Prozess, der sofort mit Grund noconnection endet, sodass ein Link oder Monitor noconnection meldet; badarg, wenn der Knoten kein Atom ist |
erlang:send(Dest, Msg, Options) | ok oder noconnect für eine Nachricht an einen anderen Knoten mit der Option noconnect; Optionen nosuspend, noconnect, priority (eine gewöhnliche Nachricht) |
send_nosuspend(Dest, Msg), send_nosuspend(Dest, Msg, [noconnect]) | true; false für einen anderen Knoten mit noconnect |
link(Pid, Options), exit(Dest, Reason, Options), exit_signal(Dest, Reason, Options) | Wie ohne Optionen; Options ist [] oder [priority] (ein gewöhnliches Signal) |
process_flag(Pid, save_calls, N) | Die vorherige Einstellung eines anderen Prozesses (0..10000); wird gespeichert, aber Clause zeichnet keine Aufrufe auf; andere Flags badarg |
processes_iterator(), processes_next(Iter) | Durchlaufen der lebenden Prozesse: {Pid, Iter1} oder none; ein während des Durchlaufs gestarteter Prozess kann erscheinen |
bump_reductions(N) | true für eine nicht negative Ganzzahl; Clause zählt keine Reduktionen |
erlang:suspend_process(Pid), erlang:suspend_process(Pid, Options), erlang:resume_process(Pid) | Siehe Prozesse anhalten |
erlang:process_display(Pid, backtrace) | true nach dem Schreiben des Aufrufstacks des lebenden Prozesses Pid auf die Standardfehlerausgabe, ein Module:Function/Arity pro Zeile, die aktuelle Funktion zuerst; badarg für einen anderen Typ oder einen beendeten Prozess |
Der neue Prozess wird am Ende der Warteschlange des Workers seines
Elternprozesses eingereiht (Worker); spawn gibt seine Pid sofort
zurück.
Clause