====== Nebenläufigkeit ======
Threads in Lyx sind dünn: ''clone'' plus futex, kein Laufzeitsystem dazwischen. Ein Thread ist eine gewöhnliche Funktion, ein Mutex ein 32-Bit-Wort. Das macht das Verhalten vorhersagbar — und verlangt, dass man weiß, was man tut.
Für Echtzeit- und Embedded-Fragen (Scheduler, Prioritäten, POSIX-AE) siehe [[lyx_-_programmiersprache:guides:rtos-embedded-concurrency|RTOS & Embedded Concurrency]]; hier geht es um den Alltag auf Linux.
→ [[lyx_-_programmiersprache:units:thread|std.thread]] · [[lyx_-_programmiersprache:sprache:threads|Threads (Sprachreferenz)]] · [[lyx_-_programmiersprache:guides:speicher-praxis|Speicher in der Praxis]]
----
===== Die Bausteine =====
^ Baustein ^ Funktionen ^
| **Thread** | ''ThreadCreate(func, arg): Thread'', ''ThreadJoin(t)'', ''ThreadIsRunning(t)'', ''ThreadSelf()'' |
| **Mutex** | ''MutexNew()'', ''MutexLock(m)'', ''MutexUnlock(m)'', ''MutexTryLock(m)'', ''MutexFree(m)'' |
| **Bedingung** | ''CondNew()'', ''CondWait(c, m)'', ''CondSignal(c)'', ''CondBroadcast(c)'', ''CondFree(c)'' |
| **Atomar** | ''AtomicNew(start)'', ''AtomicLoad(a)'', ''AtomicStore(a, v)'', ''AtomicAdd(a, d)'', ''AtomicFree(a)'' |
| **Geteilter Speicher** | ''SharedMemCreate(size)'', ''SharedMemFree(mem)'' |
| **Warten** | ''Sleep(ms)'', ''SleepNs(ns)'', ''SleepUntil(clockId, absNs)'' |
Die Thread-Funktion hat die Form ''fn(arg: int64): int64'' und wird als ''int64'' übergeben: ''ThreadCreate(Arbeiter as int64, 1)''.
----
===== Beispiel =====
Zwei Threads erhöhen denselben Zähler — einmal über einen Mutex, einmal atomar:
import std.io;
import std.alloc;
import std.thread;
import std.time;
// Gemeinsamer Zustand: hier liegt der Zaehler, den beide Threads erhoehen
var g_mutex: Mutex;
var g_zaehler: int64;
var g_atomar: Atomic;
// Ein Thread ist eine Funktion mit einem int64-Argument
fn Arbeiter(arg: int64): int64 {
var i: int64 := 0;
while (i < 10000) {
// 1. Weg: Mutex um den kritischen Abschnitt
MutexLock(g_mutex);
g_zaehler := g_zaehler + 1;
MutexUnlock(g_mutex);
// 2. Weg: atomare Addition, ohne Sperre
AtomicAdd(g_atomar, 1);
i := i + 1;
}
return 0;
}
fn main(): int64 {
g_mutex := MutexNew();
g_atomar := AtomicNew(0);
g_zaehler := 0;
var t1: Thread := ThreadCreate(Arbeiter as int64, 1);
var t2: Thread := ThreadCreate(Arbeiter as int64, 2);
// Ohne Join endet main womoeglich vor den Threads
ThreadJoin(t1);
ThreadJoin(t2);
PrintLn(StrConcat("mit Mutex: ", IntToStr(g_zaehler)));
PrintLn(StrConcat("atomar: ", IntToStr(AtomicLoad(g_atomar))));
AtomicFree(g_atomar);
MutexFree(g_mutex);
return 0;
}
mit Mutex: 20000
atomar: 20000
Übersetzt und ausgeführt mit ''lyxc 1.1.3I''.
----
===== Warum das ohne Schutz nicht geht =====
Dasselbe Programm **ohne** ''MutexLock''/''MutexUnlock'', dreimal gestartet:
mit Mutex: 16005
mit Mutex: 15624
mit Mutex: 16876
Erwartet wären 20 000. ''g_zaehler := g_zaehler + 1'' ist drei Schritte — lesen, addieren, schreiben —, und dazwischen darf der andere Thread laufen. **Die Ergebnisse sind nicht nur falsch, sie sind jedes Mal anders**: Genau daran erkennt man ein Wettrennen.
Der atomare Zähler daneben bleibt in allen Läufen korrekt.
----
===== Mutex oder atomar? =====
^ Fall ^ Mittel ^
| Ein einzelner Zähler, eine Marke | ''Atomic*'' — kein Umschalten in den Kern, kein Verklemmen möglich |
| Mehrere Werte, die zusammen stimmig bleiben müssen | Mutex um den ganzen Abschnitt |
| Warten, bis etwas fertig ist | ''Cond'' plus Mutex |
| Ergebnis eines Threads abholen | ''ThreadJoin'' — er wartet und gibt den Stapel des Kindes frei |
Faustregel: **Ein Wert → atomar. Ein Zustand → Mutex.**
----
===== Regeln, die Ärger ersparen =====
* **''ThreadJoin'' für jeden Thread.** Ohne Join endet ''main'' womöglich vor dem Thread — und ''ThreadJoin'' gibt außerdem den Stapel des Kindes frei.
* **Der kritische Abschnitt bleibt kurz.** Alles, was im Mutex passiert, hält alle anderen auf. I/O gehört nicht hinein.
* **Immer dieselbe Sperrreihenfolge.** Zwei Mutexe, in zwei Threads verschieden herum gesperrt, verklemmen sich zuverlässig.
* **''MutexUnlock'' auf jedem Rückgabepfad** — ein ''return'' mitten im Abschnitt ist die häufigste Verklemmung. ''defer'' ist hier der ruhigere Weg.
* **Kein Zeiger auf lokale Variablen** an einen Thread übergeben: Der Rahmen ist womöglich schon weg, wenn der Thread liest.
* **Alles freigeben, was angelegt wurde**: ''MutexFree'', ''CondFree'', ''AtomicFree''.
----
===== Nebenläufigkeit in einer Oberfläche =====
Eine Vega-Anwendung hat bereits eine Ereignisschleife. Dort gilt:
* **Zeitgeber statt Thread**, wenn etwas regelmäßig passieren soll — ''NewTimer(ms)'' plus ''app.AddTimer''; siehe [[lyx_-_programmiersprache:vega:praxis|Vega: Anwendungen aufbauen]].
* **Kein Control aus einem Thread anfassen.** Der Zeichenpfad gehört der Ereignisschleife. Ein Arbeitsthread legt sein Ergebnis in eine geschützte Variable; ein Zeitgeber holt es ab und ruft ''Invalidate()''.
* **Lange Arbeit nicht im Handler.** Solange ein Handler läuft, zeichnet nichts und reagiert nichts.
----
===== Fallstricke =====
* **''Sleep'' als Synchronisation.** Es macht das Wettrennen seltener, nicht unmöglich — und ist damit schlimmer als kein Schutz, weil der Fehler erst in Produktion auftritt.
* **Test hängt statt zu scheitern.** Jeder nebenläufige Test gehört unter ''timeout''.
* **Ergebnis „meistens richtig".** Ein Zähler, der bei 20 000 landen soll und 19 998 zeigt, ist kein Rundungsfehler.
Letzte Aktualisierung: 2026-08-19 — API gegen ''std/thread.lyx'' erhoben; Beispiel und Wettrennen mit ''lyxc 1.1.3I'' gemessen.