====== 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.