Praktische Multithreading-Best-Practices: C-Edition — Sicher schreiben nach Win32-API-Konvention

· · Windows, Multithreading, C, Win32 API, Geschäftsanwendungen, Fehleruntersuchung, Design

„Wir schreiben einen residenten Prozess für die Anlagensteuerung in C.“ „Wir mussten einer zwanzig Jahre alten C-Anwendung Threads hinzufügen.“ „Wir stoppen mit TerminateThread, aber gelegentlich friert der ganze Prozess ein.“ — Multithreading in C ist die Welt, in der die Sprache am wenigsten Unterstützung bietet. Es gibt weder Ausnahmen noch RAII noch Templates; die Korrektheit der Synchronisierung hängt vollständig von der Wahl der APIs und der Disziplin beim Aufrufen ab.

Dieser Artikel ist die C-Edition unserer Multithreading-Praxisreihe. Er richtet sich an Entwickler, die C gegen die Win32-API schreiben, und überträgt die Prinzipien des Multithread-Designs — mit der wahllosen Vermehrung von Threads aufhören, gemeinsam genutzten veränderlichen Zustand reduzieren, Lock-Disziplin, das Stoppen zuerst entwerfen — auf die Werkzeuge von Win32: die Art, Threads zu erzeugen (_beginthreadex), die Wahl von Synchronisierungsobjekten, ein Stopp-Design ohne TerminateThread bis hin zu den Einschränkungen von DllMain, gestützt auf Primärquellen mit Stand August 2026. Er ist so geschrieben, dass er für sich allein gelesen werden kann. Dieselben Prinzipien, für andere Sprachen ausgearbeitet, finden Sie auch in der „.NET-Edition“, der „C++-Edition“ und der „Java-Edition“.

1. Das Wichtigste zuerst

  • Erzeugen Sie Threads mit _beginthreadex, nicht mit CreateThread. Wird ein Thread, der die CRT aufruft, mit CreateThread erzeugt, kann die CRT den Prozess bei Speicherknappheit beenden.12
  • Der Standard-Lock innerhalb eines Prozesses ist der SRW-Lock; CRITICAL_SECTION nur bei Bedarf für rekursiven Erwerb. Mutex für prozessinternen Ausschluss zu verwenden, ist ein „häufiger Fehler“, der immer einen Kernelübergang mit sich bringt.3
  • Aktualisieren Sie einzelne Variablen mit den Interlocked-Funktionen. volatile garantiert weder Atomizität noch Ordnung. Die meisten Interlocked-Funktionen bringen eine vollständige Speicherbarriere mit.4
  • Warten Sie mit einer Bedingungsvariable (der Familie SleepConditionVariableCS) oder mit einem Event plus Wartefunktion. Eine Polling-Schleife mit Sleep verschwendet sowohl CPU als auch Reaktionsfähigkeit.5
  • TerminateThread verwenden wir nicht. Es ist eine gefährliche Funktion, die Locks, Heap und DLL-Zustand zerstören kann, und ist Ziel der Code-Analyse-Warnung C6258. Entwerfen Sie das Stoppen als kooperatives Stoppen mit „Stopp-Event + WaitForMultipleObjects“.67
  • Übergeben Sie die Parallelisierung kurzer Aufgaben nicht eigenen Threads, sondern dem Windows-Threadpool (CreateThreadpoolWork). Beenden Sie Threads des Pools niemals mit ExitThread / TerminateThread.89
  • In DllMain erzeugen, synchronisieren oder warten Sie keine Threads. Es wird aufgerufen, während der Loader-Lock gehalten wird, was es zu einem Nährboden für Deadlocks macht.10
  • C11s <threads.h> lässt sich ab VS 2022 17.8 verwenden, <stdatomic.h> ist jedoch noch experimentell. Bei einer reinen Windows-Codebasis ist der Win32-API-Ansatz die realistische Wahl.11

2. Warum Multithreading schwierig ist — Wettlaufsituationen und Deadlocks

Auf den Punkt gebracht bringt Multithreading, unabhängig von der Sprache, zwei Arten von Problemen mit sich.

Eine Wettlaufsituation (race condition) ist ein Fehler, bei dem sich das Ergebnis danach richtet, in welcher Reihenfolge mehrere Threads eine bestimmte Codestelle erreichen. Das klassische Beispiel ist ein gemeinsam genutzter Zähler: Der einzelne Ausdruck count++ zerfällt auf Maschinencode-Ebene in drei Schritte — Lesen → Addieren → Zurückschreiben. Treten zwei Threads gleichzeitig in diese drei Schritte ein, wird die Addition des einen durch das Zurückschreiben des anderen überschrieben und geht verloren.4 Das Ergebnis ändert sich von Lauf zu Lauf, und welches Ergebnis herauskommt, lässt sich nicht vorhersagen.

Thread BGeteilte Variable countThread AThread BGeteilte Variable countThread Acount = 10Zwei Additionen erfolgt,trotzdem count = 11 — die Addition von Thread A ging verlorenLesen (10)Lesen (10)Lokal addieren (11)Lokal addieren (11)Zurückschreiben (11)Zurückschreiben (11)

Abbildung 1: Die klassische Wettlaufsituation, bei der ein gemeinsam genutzter Zähler eine Addition verliert. Mischt sich ein anderer Thread während der drei Schritte von count++ ein, überschreibt das jeweils spätere Zurückschreiben das andere

Ein Deadlock ist ein Zustand, in dem zwei Threads jeweils auf einen Lock warten, den der andere hält, sodass keiner von beiden weiterkommt. Thread A hält Lock 1 und wartet auf Lock 2, Thread B hält Lock 2 und wartet auf Lock 1 — allein das genügt, damit beide für immer stehen bleiben.

wartet auf Freigabe von Lock 2wartet auf Freigabe von Lock 1Thread Ahält Lock 1Thread Bhält Lock 2

Abbildung 2: Die zirkuläre Wartesituation eines Deadlocks. Sobald die Wartepfeile einen Ring bilden, bleiben alle Threads in diesem Ring für immer stehen

Beide sind zeitabhängig: Eine Ausführungsreihenfolge, die auf einer Entwicklungsmaschine nur bei einem von Zehntausenden Läufen zuschlägt, kann auf einer Kundenmaschine mit anderer Kernzahl und anderem Timing jeden Tag auftreten. Dass sich ein Fehler „mit angehängtem Debugger nicht reproduzieren lässt“, liegt ebenfalls daran, dass die Beobachtung selbst das Timing verändert — ein typisches Verhalten von Wettlauf-Bugs. Deshalb zielen alle Prinzipien dieses Artikels in eine Richtung: „die Stellen reduzieren, an denen synchronisiert werden muss“, noch bevor man sich um „richtig synchronisieren“ kümmert.

2.1. C-spezifische Voraussetzungen — die Sprache schützt Sie vor nichts

Da C keinen sprachlichen Mechanismus besitzt, der diese Prinzipien erzwingt, müssen sie explizit als Disziplin niedergeschrieben werden.

Erstens: Bauen Sie die Garantie der Freigabe in die Struktur ein. Da es kein Äquivalent zu C++s RAII gibt, schützen Sie die Freigabe von Locks und das CloseHandle von Handles über das goto cleanup-Muster, das jeden Funktionsausgang auf eine Stelle bündelt, oder über eine Coding-Konvention, die Erwerb und Freigabe paarweise schreibt. Ein hinzugefügter früher return, der einen Lock leckt, ist der klassische C-Unfall.

Zweitens: Behandeln Sie Datenraces genau wie in C++. Ein einfaches Lesen oder Schreiben einer korrekt ausgerichteten 32-Bit-Variable ist unter Windows zwar atomar, doch alles darüber hinaus — 64-Bit-Variablen (auf 32-Bit-Windows), zusammengesetzte Operationen, die Konsistenz mehrerer Variablen — ist überhaupt nicht garantiert.12 Code, der „zufällig funktioniert“, bricht bei einer Änderung des Compilers oder der Optimierungsstufe.

Drittens: Legen Sie die Eigentümerschaft fest. Eine Kultur, in Funktionskommentaren ausdrücklich festzuhalten, „welcher Thread diesen Puffer beschreibt und ab wann er wem gehört“, wirkt bei Multithreading in C ebenso stark wie die Wahl der Synchronisierungsprimitive.

3. Threads erzeugen — nur _beginthreadex kommt infrage

3.1. Warum CreateThread die falsche Wahl ist

Win32s native API ist CreateThread, doch die offizielle Richtlinie lautet: Ein Thread, der CRT-Funktionen (C-Laufzeitbibliothek) aufruft, wird mit _beginthreadex erzeugt. _beginthreadex initialisiert die internen Daten, die die CRT pro Thread verwendet, bevor der Thread gestartet wird. Ruft ein mit CreateThread erzeugter Thread eine CRT-Funktion auf, kann die CRT den Prozess unter Speicherknappheit beenden.12 Da printf, malloc und strtok allesamt CRT sind, gilt in der Praxis: „Ein in C geschriebener Thread verwendet immer _beginthreadex.“

Meiden Sie auch _beginthread (ohne ex). Es hat die Falle, dass das zurückgegebene Handle ungültig wird (oder gar auf einen anderen Thread zeigen kann), wenn der erzeugte Thread früh endet — _beginthreadex, dessen Handle sich an Synchronisierungs-APIs übergeben lässt, ist die sicherere Wahl. Das von _beginthreadex zurückgegebene Handle schließt der Aufrufer mit CloseHandle.13

#include <process.h>

static unsigned __stdcall WorkerMain(void* arg)
{
    WorkerContext* ctx = (WorkerContext*)arg;
    /* ... die Warteschleife aus Kapitel 5/6 ... */
    return 0;
}

HANDLE hThread = (HANDLE)_beginthreadex(
    NULL, 0, WorkerMain, &ctx, 0, NULL);
if (hThread == NULL) { /* Fehlerbehandlung */ }
/* ...nach dem Stoppantrag... */
WaitForSingleObject(hThread, INFINITE);  /* Join */
CloseHandle(hThread);

3.2. Kurze Aufgaben gehören in den Windows-Threadpool

Wollen Sie „eine große Zahl kleiner Aufgaben verteilen“ oder erzeugen und vernichten Sie „wiederholt kurzlebige Threads“, verwenden Sie statt eigener Threads den Windows-Threadpool (die Threadpool-API ab Vista). Erzeugen Sie mit CreateThreadpoolWork ein Arbeitsobjekt und reichen Sie es mit SubmitThreadpoolWork ein, führen die Worker-Threads des Pools den Callback parallel aus.8 Die Verwaltung der Thread-Anzahl überlassen Sie dem Betriebssystem, und die Kosten für Erzeugung und Zerstörung von Threads entfallen. Das ist die Antwort, die C auf das Prinzip „keine eigenen Threads erzeugen“ gibt.

Auch die Disziplin bei der Nutzung des Pools ist offiziell festgehalten: Beenden Sie Pool-Threads nicht mit TerminateThread / ExitThread, stellen Sie in einem Callback erzeugten Zustand (TLS, Thread-Priorität und Ähnliches) vor der Rückkehr wieder her, und halten Sie Wartehandles am Leben, bis der Pool sie nicht mehr benötigt.9 Ein weiterer praktischer Hinweis: Der Pool begrenzt nur die Anzahl der Worker-Threads — noch nicht ausgeführte Callbacks, die per SubmitThreadpoolWork eingereicht wurden, können sich beliebig aufstauen. In einer dauerhaft laufenden Konfiguration, in der der Zulauf die Verarbeitung ständig überholt, setzen Sie auf Anwendungsseite eine Zugangsbegrenzung wie ein Semaphor oder eine Queue mit Kapazitätsgrenze, sodass die einreichende Seite bei Vollstand wartet oder abgewiesen wird (das ist Gegendruck, um Überlast nicht in ein Speicherproblem zu verwandeln, und folgt demselben Prinzip wie das Queue-Design in Kapitel 5).

4. Gemeinsam genutzten veränderlichen Zustand minimieren — Aufteilen, Nur-Lesen, Weitergeben

Konflikte entstehen nur, wenn „mehrere Threads“ und „gemeinsam genutzte veränderliche Daten“ zusammenkommen. Bevor Sie eine Synchronisierungsprimitive wählen (nächstes Kapitel), überlegen Sie zuerst, ob sich die gemeinsame Nutzung überhaupt reduzieren lässt. Die Mittel gliedern sich in drei Familien.

Aufteilen. Lassen Sie bei paralleler Aggregation nicht jeden Thread in einen gemeinsamen Zähler schreiben, sondern bilden Sie die Zwischensumme in einer lokalen Variable pro Thread (oder einem pro Thread zugewiesenen Puffer) und führen Sie sie am Ende genau einmal mit etwa InterlockedAdd zusammen. Schreibzugriffe auf den gemeinsamen Zustand sinken von „bei jeder Iteration“ auf „einmal pro Thread“, wodurch sowohl Synchronisierungskosten als auch das Zeitfenster für Konflikte um Größenordnungen kleiner werden. Die Eigentümerschaft aus 2.1 explizit zu machen — „wem gehört dieser Puffer“ — wird so direkt zum Bauplan der Aufteilung.

Nur lesbar machen. Konfigurationen und Tabellen, die beim Start aufgebaut und danach nicht mehr verändert werden, sind nach Abschluss der Initialisierung sicher, egal von wie vielen Threads sie gelesen werden. Schließen Sie entweder die gesamte Initialisierung ab, bevor irgendein Thread startet, oder verwenden Sie bei nötiger verzögerter Initialisierung Win32s Einmalinitialisierung (InitOnceExecuteOnce) und machen Sie die Grenze — „ab wann wird das nur lesbar“ — im Code eindeutig.3

Weitergeben. Leiten Sie den Datenfluss zwischen Threads über eine Producer/Consumer-Queue, statt eine gemeinsame Variable von beiden Seiten anzufassen. Die Implementierung in C liefert direkt das offizielle Beispiel aus der Bedingungsvariable in Kapitel 5 (ein zirkulärer Puffer fester Größe plus SleepConditionVariableCS), und ein Puffer mit Kapazitätsobergrenze wirkt zugleich als natürlicher Gegendruck: „überholt die Produktion den Konsum, wartet die Erzeugerseite“.5

5. Wahl der Synchronisierungsobjekte und Lock-Disziplin

Win32 kennt viele Arten von Synchronisierungsprimitiven, und eine falsche Wahl kostet sowohl Leistung als auch Korrektheit. Hier die offizielle Leitlinie in einem Bild zusammengefasst.3

JaAusschlussBegrenzung gleichzeitiger ZugriffeBenachrichtigung über ein EreignisNein (prozessintern)JaNeinJaNeinProzessübergreifendeSynchronisierung nötig?Wofür?Benannter MutexBenanntes SemaphorBenanntes EventRekursiver Erwerbdurch denselben Thread nötig?CRITICAL_SECTIONPortablerC++-Code?std::mutex /std::shared_mutexSRW-Lock (Standardwahl)

Abbildung 3: Wahl einer Win32-Synchronisierungsprimitive. Der erste Zweig fragt „geht es prozessübergreifend“ — der entscheidende Punkt ist, kein Kernelobjekt (Mutex) zu wählen, wenn das nicht der Fall ist

Primitive Geltungsbereich Merkmale Einsatzgebiet
SRW-Lock Prozessintern Schnell (bleibt meist im Benutzermodus), zeigergroß, nicht rekursiv Standard für neuen Code. AcquireSRWLockShared erlaubt auch gemeinsame Lesezugriffe
CRITICAL_SECTION Prozessintern Schnell (spinnt, dann Kernel-Warten), rekursiv Wenn derselbe Thread rekursiv erwerben muss
Mutex Prozessintern/prozessübergreifend Immer ein Kernelobjekt, daher langsamer Prozessübergreifender Ausschluss (benannt), Kombination mit WaitForMultipleObjects
Semaphor Prozessintern/prozessübergreifend Kernelobjekt Begrenzung gleichzeitiger Zugriffe auf einen Ressourcenpool
Event Prozessintern/prozessübergreifend Kernelobjekt Benachrichtigung „etwas ist passiert“ (nicht zum Schutz von Daten)
Interlocked-Funktionen Prozessintern (bei gemeinsamem Speicher auch prozessübergreifend) Lockfreie atomare Operationen Zähler, Flags, Zeigeraustausch4

Eine Ergänzung zu Tabelle und Flussdiagramm. Kernelobjekte wie Event, Semaphor und Mutex lassen sich, wenn Sie sie unbenannt erzeugen, ganz normal für die prozessinterne Synchronisierung verwenden (das Stopp-Event aus Kapitel 6 ist genau ein unbenanntes Event). Kernelobjekt bedeutet nicht ausschließlich prozessübergreifend. Umgekehrt bedeutet „unbenannt“ aber auch nicht zwangsläufig „strikt auf einen Prozess beschränkt“: Vererben Sie das Handle an einen Kindprozess oder duplizieren Sie es mit DuplicateHandle in einen anderen Prozess, lässt sich dasselbe Kernelobjekt auch ohne Namen aus mehreren Prozessen nutzen. Die genaue Einordnung lautet: Benennung ist eines der repräsentativen Mittel dafür, dasselbe Objekt zwischen Prozessen erneut öffnen zu können. Der Zweig in Abbildung 3 bringt den Kern der Wahl auf den Punkt — „wählen Sie für einen prozessinternen Lock kein Kernelobjekt“ — während für prozessinterne Benachrichtigung (Event) oder Begrenzung gleichzeitiger Zugriffe (Semaphor) ein unbenanntes Kernelobjekt weiterhin die richtige Antwort ist.

Die Interlocked-Familie entspricht der Interlocked-Klasse aus der .NET-Edition und std::atomic aus der C++-Edition. InterlockedIncrement / InterlockedExchange / InterlockedCompareExchange führen Operationen auf eine einzelne Variable unteilbar aus, und da die meisten dieser Funktionen eine vollständige Speicherbarriere mitbringen, erhalten Sie zugleich eine Ordnungsgarantie.4 „Mit volatile versehen, also sicher“ ist ein Missverständnis — weder Atomizität noch Ordnung sind damit garantiert (siehe FAQ). Eine weitere Voraussetzung ist die Ausrichtung (Alignment). Die von einer Interlocked-Funktion adressierte Variable muss zwingend an einer natürlichen Grenze ausgerichtet sein (bei einem 32-Bit-Wert eine 4-Byte-Grenze, bei einem 64-Bit-Wert eine 8-Byte-Grenze) — ist sie das nicht, wird das Verhalten unvorhersehbar.12 Adressieren Sie mit Interlocked niemals ein Feld in einer #pragma pack-Struktur oder in einem Puffer, der ein Kommunikationsformat direkt abbildet. Beschränken Sie Zähler und Flags auf normal deklarierte Variablen — solche, die der Compiler ausrichtet. Zudem gibt es eine eigene Vorsicht beim Austauschen von Zeigern über etwa InterlockedExchangePointer: Unteilbar ist nur der Austausch selbst, die Lebensdauer des alten Blocks nach dem Austausch schützt niemand. Lädt ein Leser den alten Zeiger genau, während ein Schreiber ihn austauscht und free aufruft, ist das ein Zugriff auf bereits freigegebenen Speicher. Ein Design, das gemeinsam genutzte Daten durch Zeigeraustausch aktualisiert, funktioniert nur zusammen mit einem Rückgewinnungsprotokoll — einem Lock, Referenzzählung oder Ähnlichem (im Zweifel ist der Schutz durch einen SRW-Lock die sichere Wahl).

Für das Warten gibt es Bedingungsvariablen. Sie werden mit InitializeConditionVariable erzeugt; die Konsumentenseite schläft mit SleepConditionVariableCS (gepaart mit CRITICAL_SECTION), die Erzeugerseite weckt mit WakeConditionVariable — genau diese Form hat das offizielle Implementierungsbeispiel einer Producer/Consumer-Queue über einem Puffer fester Größe.5 Wichtige Vorgehensweise: Prüfen Sie nach dem Aufwachen die Bedingung (ist die Queue nicht leer) stets erneut innerhalb des Locks, und kehren Sie bei „falsch“ zum Warten zurück — als Schleife. Bedingungsvariablen kennen Spurious Wakeups, das Aufwachen ohne Benachrichtigung, und beim Aufwachen kann bereits ein anderer Konsument das Element vorweggenommen haben — „geweckt“ bedeutet also nicht zwangsläufig „Bedingung erfüllt“. Das ist das Werkzeug, um in C dieselbe Struktur wie den Channel aus 4.3 der .NET-Edition und die BlockingQueue aus Kapitel 4 der C++-Edition aufzubauen. In Kombination mit einem SRW-Lock verwenden Sie SleepConditionVariableSRW.

5.1. Lock-Disziplin — drei Prinzipien, die unabhängig von der Wahl gelten

Auch die richtige Wahl der Primitive verhindert Konflikte nicht, wenn die Nutzungsdisziplin fehlt.

  • Legen Sie eine 1:1-Zuordnung fest, welcher Lock welche Daten schützt. Ordnen Sie jeder Menge zu schützender veränderlicher Daten genau einen Lock zu (einen SRW-Lock oder eine CRITICAL_SECTION), und nehmen Sie an jeder Stelle, die diese Daten anfasst, denselben Lock. Die Praxis, im Header-Kommentar explizit festzuhalten „diese Struktur wird von g_lockFoo geschützt“, wirkt in C besonders stark.
  • Tun Sie bei gehaltenem Lock nichts Langwieriges oder Externes. Während Sie einen Lock halten, dürfen Sie ausschließlich die geschützten Daten lesen und schreiben. Datei-E/A, Netzwerkaufrufe oder Callback-Aufrufe bei gehaltenem Lock verlängern nicht nur die Haltezeit, sondern erzeugen auch, wenn die aufgerufene Stelle einen anderen Lock zu nehmen versucht, die zirkuläre Wartesituation aus Abbildung 2.
  • Legen Sie die Erwerbsreihenfolge mehrerer Locks fest. Machen Sie es überall dort, wo zwei oder mehr Locks genommen werden, zur Regel, dass alle Threads sie in derselben Reihenfolge (einer Lock-Hierarchie) erwerben. Dass eine umgekehrte Reihenfolge (lock order inversion) schwer zu debuggende Deadlocks erzeugt und dass eine Hierarchie definiert und konsequent befolgt werden sollte, hält das Best-Practices-Dokument zu DLLs ausdrücklich fest.10

6. Das Stoppen entwerfen — ohne TerminateThread

6.1. Was TerminateThread zerstört

TerminateThread löscht den Zielthread, ohne ihn auch nur eine Zeile Usermode-Code ausführen zu lassen. Die von der offiziellen Dokumentation aufgezählten Folgen sind schwerwiegend. Hielt der Zielthread eine kritische Sektion, wird sie nie freigegeben; war er gerade bei einer Heap-Operation, bleibt der Heap-Lock gehalten (jeder Thread, der danach malloc aufruft, hängt); manipulierte er gerade den globalen Zustand einer DLL, wird dieser Zustand zerstört. „Eine gefährliche Funktion, die nur im äußersten Notfall verwendet werden sollte“ ist die offizielle Einordnung, und auch die Code-Analyse erkennt sie als Warnung C6258.67

Dass sich bei der Ursachenuntersuchung einer Anwendung, die „gelegentlich ganz einfriert“, TerminateThread findet, ist in der Praxis ein wirklich häufiger Anblick. Finden Sie es, gehört es repariert.

6.2. Die richtige Form: Stopp-Event + WaitForMultipleObjects

Das etablierte Muster für kooperatives Stoppen in C besteht darin, ein einziges manuell zurückzusetzendes Stopp-Event zu erzeugen, auf das jeder Worker-Thread gleichzeitig mit dem „Arbeitssignal“ wartet. Die Dokumentation zu Warnung C6258 selbst verweist auf genau diese Form — ein Event erzeugen, jeder Thread überwacht es mit WaitForSingleObject und beendet sich selbst — als korrekte Methode zum Beenden.7

HANDLE hStopEvent;   /* CreateEvent(NULL, TRUE, FALSE, NULL): manuell zurückgesetzt */
HANDLE hWorkEvent;   /* CreateEvent(NULL, FALSE, FALSE, NULL): automatisch zurückgesetzt.
                        Kehrt beim Empfang automatisch in den nicht signalisierten
                        Zustand zurück (bei manuellem Reset würde das Warten nach
                        einmaligem Signalisieren einfach durchlaufen und zu einer
                        Busy-Loop werden, die endlos über eine leere Queue kreist) */

static unsigned __stdcall WorkerMain(void* arg)
{
    HANDLE waits[2] = { hStopEvent, hWorkEvent };
    for (;;) {
        DWORD r = WaitForMultipleObjects(2, waits, FALSE, INFINITE);
        if (r == WAIT_FAILED) {          /* z. B. ungültiges Handle. Unbehandelt dreht das mit voller Kraft leer */
            LogLastError();              /* GetLastError() protokollieren und aussteigen */
            break;
        }
        if (r == WAIT_OBJECT_0)          /* Stoppantrag */
            break;
        if (r == WAIT_OBJECT_0 + 1) {    /* Arbeit vorhanden */
            /* Stopp-Event auch an ProcessNextItem übergeben: Wartet ein einzelner
               Auftrag intern lange, hält das Herunterfahren als Geisel dieses
               einen Auftrags, wenn der Stopp dort nicht beobachtet werden kann */
            while (ProcessNextItem(hStopEvent)) {  /* Verarbeitet ein Element aus der Queue. FALSE, wenn leer */
                /* Stoppantrag auch während des Abarbeitens prüfen. Wird das
                   versäumt, ist kein Stoppen möglich, solange Arbeit weiter
                   aufläuft (Stopp-Verhungern) */
                if (WaitForSingleObject(hStopEvent, 0) == WAIT_OBJECT_0)
                    break;
            }
        }
    }
    Cleanup();                           /* Räumt selbst auf */
    return 0;                            /* Endet selbst */
}

BOOL StopWorkers(HANDLE* threads, DWORD count)
{
    BOOL ok = TRUE;
    if (!SetEvent(hStopEvent)) {             /* Ist der Stoppantrag nicht angekommen, */
        LogLastError();                      /* nicht in ein unbegrenztes Join eintreten */
        return FALSE;
    }
    /* Bei allen steht jetzt gleichzeitig der Stoppantrag */
    for (DWORD i = 0; i < count; i++) {
        if (WaitForSingleObject(threads[i], INFINITE) == WAIT_OBJECT_0) {
            CloseHandle(threads[i]);         /* Nur die Handles schließen, deren Join bestätigt wurde */
        } else {
            LogLastError();                  /* WAIT_FAILED: z. B. ungültiges Handle */
            ok = FALSE;                      /* Nicht melden, dass „alle gestoppt“ sind */
        }
    }
    return ok;   /* Bei FALSE nicht mit der Freigabe gemeinsam genutzter Ressourcen fortfahren */
}

Dass die stoppende Seite mit einzelnen WaitForSingleObject-Aufrufen joint, hat einen Grund. WaitForMultipleObjects kann höchstens MAXIMUM_WAIT_OBJECTS (64) Handles gleichzeitig erwarten; übergeben Sie ein größeres Array, scheitert das Warten selbst mit WAIT_FAILED, sodass Sie Handles schließen, obwohl Sie glaubten, auf alle gewartet zu haben, während Sie in Wahrheit auf niemanden gewartet haben. Wollen Sie nur auf das Ende aller warten, ist eine Schleife ohne Obergrenze, die einzeln nacheinander wartet, die sichere Wahl.

SetEvent(hStopEvent)Stoppende SeiteStopp-Event(manuell zurückgesetzt: für alle sichtbar)Worker 1:wartet mit WaitForMultipleObjectsgleichzeitig auf Stopp und ArbeitWorker 2:wartet mit WaitForMultipleObjectsgleichzeitig auf Stopp und ArbeitRäumt auf und returnt selbstRäumt auf und returnt selbstStoppende Seite wartet auf die Thread-Handles und jointerst jetzt gilt „gestoppt“

Abbildung 4: Das Stopp-Event-Muster. Ein manuell zurückgesetztes Event für das Stoppen zu verwenden bedeutet, dass ein einziges SetEvent alle wartenden Worker gleichzeitig weckt. Wie er endet, entscheidet jeder Thread selbst, und erst mit dem Abschluss des Zusammenführens gilt er als gestoppt

Drei Punkte sind entscheidend. Machen Sie das Stopp-Event manuell zurückgesetzt (ein einziges SetEvent ist dann für alle Worker sichtbar); stellen Sie das Stopp-Event an den Anfang des Wartearrays (wird bei gleichzeitiger Signalisierung dann bevorzugt); und die stoppende Seite wartet immer erst auf das Joinen der Thread-Handles, bevor sie sie schließt.

Zwei Vorbehalte zum Geltungsbereich. Erstens: Dieses Muster „Event plus vollständiges Abarbeiten“ ist für eine Konfiguration mit einem einzelnen Worker gedacht. Ein automatisch zurückgesetztes Event kann, egal wie oft Sie SetEvent aufrufen, immer nur „ein signalisierter Zustand liegt vor“ ausdrücken (aufeinanderfolgende Signale verschmelzen), sodass bei mehreren Workern nur einer aufwacht und den ganzen Schub seriell abarbeitet. Teilen sich mehrere Worker eine Queue, ersetzen Sie das Arbeitssignal durch ein Semaphor und erhöhen Sie bei jedem eingereihten Element den Zähler mit ReleaseSemaphore(hSem, 1, NULL). Ein erfolgreiches Semaphor-Warten verbraucht genau eine Zähleinheit, sodass sich die korrekte Entsprechung ergibt: „genau so viele wartende Worker wachen einzeln auf, wie Elemente eingereiht wurden“ (dieser Einsatzzweck fällt ebenso wie die „Begrenzung gleichzeitiger Zugriffe auf einen Ressourcenpool“ aus der Tabelle in Abbildung 3 in das Aufgabengebiet des Semaphors). Wechseln Sie jedoch zum Semaphor, ändern Sie auch die Konsumentenseite auf „ein erfolgreiches Warten = genau ein Element aus der Queue verarbeiten“. Lassen Sie die Abarbeitungsschleife aus dem obigen Beispiel unverändert stehen, leert ein einzelnes Warten — das nur eine Berechtigung verbraucht — die gesamte Queue, und die Buchhaltung gerät durcheinander: Andere Worker wachen mit den übrig gebliebenen Berechtigungen zu einer leeren Queue auf, und das ReleaseSemaphore der Erzeugerseite schlägt fehl, weil das Maximum überschritten wird. Die Entsprechung „eine Berechtigung = ein Auftrag“ einzuhalten, ist die Voraussetzung des Semaphor-Ansatzes. Zweitens: Führen Sie den Stopp-Pfad auch durch die Verarbeitung eines einzelnen Elements. Wartet ProcessNextItem intern lange blockierend, übergeben Sie auch dort das Stopp-Event und warten Sie überlagert, oder versehen Sie es mit einem endlichen Timeout. Eine Prüfung nur zwischen den Elementen lässt das Loch offen, dass „das Herunterfahren ewig wartet, weil ein einzelnes Element nicht fertig wird“. Das ist dasselbe, was StopAsync in der .NET-Edition und jthread plus join in der C++-Edition aussagen.

Ein Thread, der auf blockierende E/A wartet (Pipe, Socket, serielle Schnittstelle), kann nicht zwischendurch nach dem Event sehen — deshalb braucht auch die E/A-Seite ein eigenes Design: entweder ein überlagertes Warten aus OVERLAPPED-E/A plus einem Event, oder das Aufwecken der E/A mit CancelIoEx (ein konkretes Beispiel für serielle Kommunikation finden Sie in „Fallstricke von Anwendungen mit serieller Kommunikation“).

7. DllMain und der Loader-Lock — das Minenfeld beim Schreiben von DLLs

Gemeinsam genutzte Komponenten, die in C geschrieben werden, enden häufig als DLL, und dort gilt eine eigene Einschränkung: der Loader-Lock. Der OS-Loader ruft DllMain auf, während er den Loader-Lock hält, sodass die folgenden Aktionen darin zu Deadlocks oder Abstürzen führen.10

  • Synchronisierung mit anderen Threads (Lock-Erwerb, Warten auf das Ende eines Threads)
  • Der Aufruf von LoadLibrary / FreeLibrary (direkt wie indirekt)
  • Das Erzeugen von Threads (gefährlich, sofern es Synchronisierung einschließt) oder ExitThread

„Beim Entladen der DLL in DllMain auf das Ende eines Worker-Threads warten“ sieht durchaus korrekt aus, ist aber ein klassischer Deadlock (der endende Thread versucht seinerseits, für die Zustellung von DLL_THREAD_DETACH den Loader-Lock zu nehmen, und beide Seiten warten aufeinander). Eine DLL mit eigenen Threads sollte explizite Initialisierungs- und Beendigungsfunktionen wie MyLib_Init / MyLib_Shutdown bereitstellen und Start sowie Join der Threads dort vornehmen. Das Ideal für DllMain ist ein nahezu leerer Stub.10

8. Die Option C11-Threads — der aktuelle Stand

Wenn Sie „portables C schreiben wollen, das nicht von Win32 abhängt“, sind C11s <threads.h> (thrd_create / mtx_lock / cnd_wait) und <stdatomic.h> die Option. Der Unterstützungsstand in MSVC laut der offiziellen Konformitätstabelle: <threads.h> wird ab Visual Studio 2022 17.8 unterstützt (erfordert /std:c11 und ein passendes SDK), während <stdatomic.h> noch experimentell ist und die Option /experimental:c11atomics benötigt.11

Ist gemeinsamer Code mit Linux eine Anforderung, haben C11-Threads (oder ein pthread-Wrapper) ihren Wert; bei einer reinen Windows-Codebasis ist der in diesem Artikel behandelte Win32-Ansatz jedoch im Hinblick auf Informationsmenge, Erfahrungswerte und Debugbarkeit im Vorteil. Für welchen Weg Sie sich auch entscheiden — die bisherigen Designprinzipien (gemeinsame Nutzung reduzieren, Zuordnung von Lock und Daten, kooperatives Stoppen) ändern sich nicht.

9. Verifikation und Debugging — Vorbereitung unter der Annahme, dass es sich nicht reproduzieren lässt

Von Tests lässt sich nicht erwarten, dass sie Wettlauf-Bugs finden, denn ein gewöhnlicher Test zählt einen Lauf, bei dem „zufällig kein Konflikt auftrat“, als Erfolg. Denken Sie die Vorsorge in drei Schichten.

Die erste Verteidigungslinie ist das Design. Prüfen Sie in der Review anhand einer Tabelle: welche veränderlichen Daten gemeinsam genutzt werden, welcher Lock jeweils schützt (die Zuordnung aus 5.1), ob die Erwerbsreihenfolge der Locks eindeutig ist und ob das Stopp-Event alle Worker erreicht. Ein Design, für das sich diese Tabelle nicht ausfüllen lässt, ist noch nicht fertig, auch wenn es gerade läuft.

Zweitens machen Sie Anomalien beobachtbar. Warten Sie an entscheidenden Stellen nicht bedingungslos mit INFINITE, sondern versehen Sie das Warten mit einem Timeout und protokollieren Sie das Überschreiten — so verwandeln Sie einen ewigen Hänger in einen erkennbaren Fehlschlag. Erfassen Sie bei einem Hänger vor Ort einen Dump, prüfen Sie die Stacks aller Threads und sehen Sie nach, ob sich gegenseitige Lock-Wartezeiten zu einem Kreis schließen. Für Fehler rund um DLLs wird eine Prüfung mit Application Verifier offiziell empfohlen.10 Aufbau und Pflege von Dumps und Logs behandeln wir in „Design für Logs und Dumps bei Abstürzen von Windows-Apps“.

Drittens schütteln Sie das System unter Last durch. Stresstests — längeres Laufenlassen mit mehr Threads als vorhandenen Kernen, das Randomisieren der Verarbeitungsreihenfolge, das Einfügen künstlicher Verzögerungen — sind ein praktisches Mittel, um auf der Entwicklungsmaschine leichter einen Konflikt „zu treffen“. Vergessen Sie auch nicht den Reproduktionstest in einem optimierten Release-Build unter hoher Last.

10. Zusammenfassung — Checkliste für C

  1. Wird jeder Thread mit _beginthreadex erzeugt (mischen sich keine CreateThread / _beginthread darunter)?
  2. Werden Thread-Handles erst gejoint (WaitForSingleObject) und dann mit CloseHandle geschlossen?
  3. Werden für kurzlebige Aufgaben nicht wahllos eigene Threads erzeugt (ließen sie sich der Threadpool-API übergeben)?
  4. Erfolgt prozessinterner Ausschluss über SRW-Lock/CRITICAL_SECTION (statt eines missbrauchten Mutex)?
  5. Werden gemeinsame Zähler und Flags mit Interlocked-Funktionen statt mit bloßem volatile aktualisiert?
  6. Ist keine Sleep-Polling-Schleife mehr übrig (wurde sie durch Bedingungsvariable/Event-Warten ersetzt)?
  7. Gibt es an keiner einzigen Stelle TerminateThread (das erzwungene Beenden eines anderen Threads)? Enden Worker über ein return aus der Thread-Funktion statt über einen Aufruf von ExitThread (sodass die CRT-Nachbereitung korrekt über _endthreadex läuft)?
  8. Verfügt jeder Worker über einen Stopp-Pfad aus Stopp-Event + WaitForMultipleObjects, und lassen sich auch Threads wecken, die in blockierender E/A stecken?
  9. Ist die Freigabe von Locks und Handles auf jedem Rückgabepfad garantiert (die goto cleanup-Disziplin)?
  10. Werden in DllMain keine Threads erzeugt, synchronisiert oder auf ihr Ende gewartet?

Im Austausch für fehlende sprachliche Unterstützung ist die Qualität von Multithreading in C genau das, was Ihre API-Wahl und Ihre Disziplin daraus machen. Machen Sie _beginthreadex, SRW-Lock, Interlocked-Funktionen und das Stopp-Event zu Ihrem Standard-Vierergespann, und selbst in C gelingt ein Design, das Abstand vom „gelegentlichen Einfrieren“ hält.

Verwandte Artikel

Verwandte Beratungsleistungen

Die KomuraSoft LLC übernimmt Multithreading-Design-Reviews für in C geschriebene residente Prozesse, Anlagensteuerungsanwendungen und DLLs, die Ursachenuntersuchung (Dump-Analyse) von Hängern und Abstürzen, die durch TerminateThread oder leckende Locks verursacht werden, sowie die technische Beratung zum Hinzufügen von Threads zu Legacy-C-Code.

  1. Microsoft Learn, CreateThread function. Dazu, dass Threads innerhalb einer ausführbaren Datei, die die CRT aufruft, mit _beginthreadex / _endthreadex verwaltet werden sollten statt mit CreateThread / ExitThread; sowie dazu, dass die CRT den Prozess unter Speicherknappheit beenden kann, wenn ein mit CreateThread erzeugter Thread die CRT aufruft.  2

  2. Microsoft Learn, Multithreading with C and Win32. Dazu, dass Programme, die die CRT-Bibliothek aufrufen, Threads mit _beginthread / _beginthreadex starten sollten und nicht mit Win32s CreateThread / ExitThread; dazu, dass die _beginthread-Familie die CRT-internen Pro-Thread-Variablen initialisiert; sowie dazu, dass SuspendThread einen Thread anhalten kann, während er auf interne CRT-Datenstrukturen zugreift, was zu einem Deadlock führen kann.  2

  3. Microsoft Learn, About Synchronization. Zu den Leitlinien für die Wahl von Win32-Synchronisierungsprimitiven: Der SRW-Lock ist der Standard für neuen Code, zeigergroß und bleibt normalerweise im Benutzermodus; CRITICAL_SECTION wird bei Bedarf für rekursiven Erwerb verwendet; Mutex ist immer ein Kernelobjekt und wird für benannte prozessübergreifende Synchronisierung sowie in Kombination mit WaitForMultipleObjects genutzt; die Verwendung von Mutex für prozessinterne Synchronisierung ist ein „häufiger Fehler“, der bei häufigen Operationen erheblich langsamer ist; sowie dazu, dass Semaphore die gleichzeitige Zugriffszahl auf einen Ressourcenpool begrenzen und Events der Benachrichtigung dienen.  2 3

  4. Microsoft Learn, Interlocked Variable Access. Dazu, dass Interlocked-Funktionen den Zugriff auf eine von mehreren Threads gemeinsam genutzte Variable synchronisieren und die Operation unteilbar ausführen; dazu, dass InterlockedIncrement / Decrement Lesen, Addieren und Zurückschreiben zu einer atomaren Operation bündeln, da ohne Synchronisierung eine gleichzeitige Inkrementierung durch zwei Threads eine Erhöhung verlieren kann; zur Funktionsfamilie InterlockedExchange / InterlockedCompareExchange und Ähnlichem; dazu, dass sich das bei einer Variable im gemeinsam genutzten Speicher auch zwischen Threads verschiedener Prozesse verwenden lässt; sowie dazu, dass die meisten Interlocked-Funktionen eine vollständige Speicherbarriere bereitstellen und sich mit Acquire-/Release-Varianten die Ordnungssemantik wählen lässt.  2 3 4

  5. Microsoft Learn, Using Condition Variables. Zum Implementierungsbeispiel einer Producer/Consumer-Queue über einen durch CRITICAL_SECTION geschützten zirkulären Puffer fester Größe; zur Struktur, bei der InitializeConditionVariable eine Bedingungsvariable erzeugt, der Konsument mit SleepConditionVariableCS wartet und der Produzent mit WakeConditionVariable weckt; sowie zur Unterstützung von Bedingungsvariablen ab Windows Vista.  2 3

  6. Microsoft Learn, TerminateThread function. Dazu, dass TerminateThread den Zielthread beendet, ohne ihn irgendeinen Usermode-Code ausführen zu lassen; dazu, dass eine vom Ziel gehaltene kritische Sektion nicht freigegeben wird; dazu, dass der Heap-Lock nicht freigegeben wird, wenn gerade Speicher vom Heap alloziert wurde; dazu, dass der Zustand von kernel32 oder der globale Zustand einer DLL zerstört werden kann; sowie dazu, dass es sich um eine „gefährliche Funktion handelt, die nur im äußersten Notfall verwendet werden sollte“, die nur aufgerufen werden sollte, wenn Sie jeden Code, den der Zielthread ausführen könnte, vollständig kennen und kontrollieren.  2

  7. Microsoft Learn, Warning C6258. Dazu, dass die Code-Analyse-Warnung C6258 die Verwendung von TerminateThread erkennt; dazu, dass mit TerminateThread keine angemessene Thread-Bereinigung möglich ist; sowie dazu, dass als korrekte Beendigungsmethode gezeigt wird, mit CreateEvent ein Event zu erzeugen, jeden Thread den Event-Zustand mit WaitForSingleObject überwachen zu lassen und die Ausführung selbst zu beenden, sobald das Event signalisiert wird.  2 3

  8. Microsoft Learn, CreateThreadpoolWork function. Dazu, dass mit CreateThreadpoolWork ein Arbeitsobjekt erzeugt wird und ein Worker-Thread des Pools bei jedem Aufruf von SubmitThreadpoolWork den Callback ausführt; dazu, dass sich über eine Callback-Umgebung (TP_CALLBACK_ENVIRON) die Ausführungsumgebung angeben lässt; sowie zur Verfügbarkeit ab Windows Vista.  2

  9. Microsoft Learn, Thread Pools. Dazu, dass sich der Threadpool für Anwendungen eignet, die eine große Zahl kurzer Aufgaben asynchron ausführen, oder die häufig kurzlebige Threads erzeugen; zu den Bestandteilen der in Vista neu entworfenen Threadpool-API; sowie als Best Practice dazu, Pool-Threads nicht mit TerminateThread zu beenden oder ExitThread aus einem Callback aufzurufen, in einem Callback erzeugten Zustand vor der Rückkehr aufzuräumen, und Wartehandles am Leben zu halten, bis der Pool sie nicht mehr benötigt.  2

  10. Microsoft Learn, Dynamic-Link Library Best Practices. Dazu, dass DllMain aufgerufen wird, während der Loader-Lock gehalten wird, was gravierende Einschränkungen für die aufrufbaren APIs mit sich bringt; dazu, dass eine Synchronisierung mit anderen Threads innerhalb von DllMain zu einem Deadlock führen kann; dazu, dass der Aufruf von LoadLibrary auf der Liste verbotener Handlungen steht; zum Muster, bei dem das Warten in DllMain auf das Ende eines Threads beim Entladen der DLL gegen den Versuch des endenden Threads, für die Zustellung von DLL_THREAD_DETACH den Loader-Lock zu nehmen, in einen Deadlock läuft; dazu, dass das ideale DllMain ein nahezu leerer Stub ist und Initialisierung so weit wie möglich verzögert werden sollte; sowie dazu, dass eine Lock-Hierarchie definiert werden sollte, in der der Loader-Lock ganz oben steht.  2 3 4 5

  11. Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version. Dazu, dass laut der Konformitätstabelle für die C-Standardbibliothek C11-Threads (threads.h) ab Visual Studio 2022 17.8 unterstützt werden; dazu, dass stdatomic.h als experimentell behandelt wird (Option /experimental:c11atomics); sowie dazu, dass die Compiler-Unterstützung für C11 / C17 Visual Studio 2019 16.8 oder neuer sowie ein passendes Windows SDK erfordert.  2

  12. Microsoft Learn, Interlocked Variable Access. Dazu, dass ein einfaches Lesen oder Schreiben einer korrekt ausgerichteten 32-Bit-Variable atomar ist, die Synchronisierung (Ordnung) des Zugriffs aber nicht garantiert wird; dazu, dass ein einfaches Lesen oder Schreiben einer 64-Bit-Variable auf 64-Bit-Windows atomar ist, auf 32-Bit-Windows aber nicht garantiert wird; sowie dazu, dass für Variablen anderer Größen auf keiner Plattform Atomizität garantiert ist.  2

  13. Microsoft Learn, _beginthread, _beginthreadex. Dazu, warum _beginthreadex sicherer ist als _beginthread: Bei einem mit _beginthread erzeugten Thread kann das zurückgegebene Handle ungültig werden (oder auf einen anderen Thread zeigen), wenn er früh endet; das Handle von _beginthreadex muss der Aufrufer mit CloseHandle schließen, wodurch seine Gültigkeit garantiert ist; mit _beginthreadex lässt sich das Handle an Synchronisierungs-APIs übergeben; die Thread-Funktion gibt unter der __stdcall-Konvention einen Thread-Beendigungscode zurück; sowie dazu, dass gegen die Multithread-Version der CRT gelinkt werden muss. 

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Sollte ich CreateThread oder _beginthreadex verwenden?
Für einen Thread, der Funktionen der C-Laufzeitbibliothek (CRT) aufruft, ist es _beginthreadex. _beginthreadex initialisiert die internen Daten, die die CRT pro Thread benötigt, bevor der Thread gestartet wird. Die offizielle Dokumentation hält ausdrücklich fest, dass die CRT den Prozess bei Speicherknappheit beenden kann, wenn ein mit CreateThread erzeugter Thread eine CRT-Funktion aufruft. In der Praxis rufen Threads einer in C geschriebenen Anwendung so gut wie sicher irgendwo eine CRT-Funktion auf (printf, malloc, strtok und Ähnliches), sodass Sie sich unbedenklich die Regel „immer _beginthreadex“ merken können. Meiden Sie zudem _beginthread (ohne ex): Es hat die Falle, dass das Handle ungültig wird, wenn der erzeugte Thread früh endet — auch deshalb wählen Sie _beginthreadex.
Darf ich einen Thread nicht mit TerminateThread stoppen?
Nein. TerminateThread löscht den Zielthread, ohne ihn auch nur eine Zeile Usermode-Code ausführen zu lassen: Hielt dieser Thread eine kritische Sektion, wird sie nie freigegeben; war er gerade dabei, Speicher vom Heap zu allozieren, bleibt der Heap-Lock gehalten; manipulierte er gerade den globalen Zustand einer DLL, wird dieser Zustand zerstört. Die offizielle Dokumentation nennt es ausdrücklich eine „gefährliche Funktion, die nur im äußersten Notfall verwendet werden sollte“, und auch die Code-Analyse erkennt es als Warnung C6258. Das korrekte Stoppen ist kooperatives Stoppen: Sie erzeugen ein Stopp-Event, jeder Thread überwacht dieses Event mit WaitForSingleObject / WaitForMultipleObjects und räumt selbst auf, bevor er endet.
Ich habe für den prozessinternen Ausschluss einen Mutex verwendet. Was ist daran problematisch?
Es funktioniert, kostet aber erheblich Leistung. Ein Win32-Mutex ist immer ein Kernelobjekt, sodass bei jedem Erwerb und jeder Freigabe ein Übergang in den Kernelmodus stattfindet. Für prozessinternen Ausschluss sind ein SRW-Lock oder eine CRITICAL_SECTION — die im Benutzermodus verbleiben und nur bei Konflikten in ein Kernel-Warten fallen — erheblich schneller, und auch die offizielle Dokumentation nennt die Verwendung von Mutex für prozessinterne Synchronisierung ausdrücklich einen „häufigen Fehler“. Der Mutex kommt zum Einsatz, wenn Sie als benanntes Objekt prozessübergreifenden Ausschluss brauchen oder wenn Sie mit WaitForMultipleObjects gleichzeitig mit anderen Kernelobjekten warten wollen.
Lassen sich C11s threads.h und stdatomic.h unter Windows verwenden?
In MSVC werden C11-Threads (threads.h) seit Visual Studio 2022 17.8 unterstützt (erfordert /std:c11 und ein passendes Windows SDK). stdatomic.h dagegen gilt als experimentell und befindet sich noch in dem Stadium, in dem die Option /experimental:c11atomics erforderlich ist (nach der offiziellen Konformitätstabelle mit Stand August 2026). Das ist eine Option, wenn Portabilität oberste Priorität hat; bei einer reinen Windows-Codebasis ist es angesichts Erfahrungswerten und Informationsmenge jedoch realistischer, mit der Win32-API (_beginthreadex, SRW-Lock, Bedingungsvariablen, Interlocked) zu schreiben.
Wird ein gemeinsam genutztes Flag sicher, wenn ich volatile davorsetze?
Nein. Das volatile von C unterdrückt lediglich Compiler-Optimierungen wie das Zwischenspeichern eines Werts in einem Register — es garantiert weder die Atomarität einer Operation noch eine Speicherordnung über Prozessoren hinweg. Ein einfaches Lesen oder Schreiben einer korrekt ausgerichteten 32-Bit-Variablen ist unter Windows für sich genommen atomar, aber „lesen, addieren und zurückschreiben“ zerfällt in einzelne Schritte, und auch über dessen Reihenfolge relativ zu umgebenden Speicheroperationen ist nichts zugesichert. Verwenden Sie zum Aktualisieren eines gemeinsam genutzten Zählers oder Flags die Interlocked-Funktionsfamilie. Die meisten Interlocked-Funktionen tragen eine vollständige Speicherbarriere, sodass Sie die Ordnungsgarantie gleich mit erhalten. Wenn Sie mehrere Variablen gemeinsam schützen müssen, nehmen Sie eine SRW-Sperre oder eine CRITICAL_SECTION.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog