Praktische Multithreading-Best-Practices: C-Edition — sicher schreiben nach der Win32-API
· Aktualisiert am: · Go Komura · Windows, Multithreading, C, Win32 API, Geschäftsanwendungen, Fehleruntersuchung, Design
Änderungsverlauf (Erstfassung, veröffentlicht am 2. Aug 2026)
- Erstveröffentlichung
Diesen Artikel zitieren(DOI (registriertes Archiv): 10.5281/zenodo.22175887)
Die folgenden DOIs verweisen auf bereits archivierte Versionen, die vom aktuellen Text abweichen können. Verwenden Sie die URL dieser Seite, um auf den aktuellen Text zu verweisen.
Go Komura (2026). Praktische Multithreading-Best-Practices: C-Edition — sicher schreiben nach der Win32-API. KomuraSoft LLC. https://comcomponent.com/de/blog/multithreading-best-practices-c/
- DOI (registriertes Archiv)
- 10.5281/zenodo.22175887
- DOI (zuletzt registrierte Version)
- 10.5281/zenodo.22175888
„Ein in C geschriebener residenter Prozess hängt nur beim Beenden.“ „Wir wollen einer alten Anlagensteuerungsanwendung Threads hinzufügen.“ „Wir stoppen mit TerminateThread, aber gelegentlich reagiert der ganze Prozess nicht mehr.“ Wenn Multithreading in C geschrieben wird, geht es weniger darum, Arbeit nebeneinander laufen zu lassen, als darum, wer die gemeinsamen Daten schützt, wie Threads warten und wie sie enden.
C hat keinen Mechanismus wie das RAII von C++, der Lock oder Handle automatisch freigibt, sobald ein Gültigkeitsbereich verlassen wird. Auch Ausnahmen und Templates helfen nicht, deshalb reicht die Wahl der richtigen APIs nicht: Die Disziplin von Erwerb, Freigabe und Warten auf das Ende muss in die Struktur des Codes eingebaut werden.
Dieser Artikel ist die C-Edition der Multithreading-Praxisreihe. Er richtet sich an Entwickler, die C und die Win32-API verwenden, und überträgt die Prinzipien — Threads nicht wahllos vermehren, gemeinsam genutzten veränderlichen Zustand reduzieren, Lock-Disziplin festlegen, das Stoppen zuerst entwerfen — auf konkrete APIs.
Behandelt werden die Erzeugung mit _beginthreadex, die Wahl der Synchronisierungsobjekte, kooperatives Stoppen ohne TerminateThread und die Einschränkungen von DllMain. Der Informationsstand ist August 2026. Der Artikel lässt sich für sich lesen; dieselben Prinzipien sind für andere Sprachen in der „.NET-Edition“, der „C++-Edition“ und der „Java-Edition“ ausgearbeitet.
Vom Problem aus lesen
| Was Sie beschäftigt | Zuerst prüfen | Leseabschnitt |
|---|---|---|
| Threads hinzufügen / viele kurze Aufgaben parallel ausführen | Wann eigene Threads, wann der Threadpool | Threads erzeugen |
| Zähler stimmen nicht / gemeinsame Daten gehen kaputt | Entwürfe, die Teilen reduzieren, plus passende Locks und atomare Operationen | Gemeinsamen Zustand reduzieren, Synchronisierung wählen |
| Nach dem Einfügen eines Locks hängt es | Was der Lock schützt, was bei gehaltenem Lock läuft, Erwerbsreihenfolge | Lock-Disziplin |
| Hängt beim Beenden / erzwungenes Beenden im Einsatz | Stoppantrag und Endbestätigung trennen, ob wartende Threads noch stoppen können | Kooperatives Stoppen |
| Events wecken, aber die Arbeit verteilt sich ungleich | Unterschied der Benachrichtigung bei einem Worker und bei mehreren | Geltungsbereich des Stopp-Beispiels |
| Hängt beim Entladen der DLL | Ob Start, Stopp und Join außerhalb von DllMain liegen | Einschränkungen von DllMain |
| Code mit Linux teilen / Fehler nicht reproduzierbar | Portabilitätsanforderung plus Design-Review, Logs und Lasttest | Die Option C11, Verifikation und Debugging |
Beim ersten Entwurf halten Sie in Kapitel 2 die C-Voraussetzungen fest, legen in den Kapiteln 3 bis 5 die Ausführungseinheiten und die gemeinsamen Daten fest und prüfen in Kapitel 6 den Stopp-Pfad. Zur Prüfung bestehenden Codes eignet sich auch die Checkliste am Ende.
1. Zuerst die Schlussfolgerung
Den Ausführungsort nach der Art der Arbeit wählen
Eigene Threads, die die CRT aufrufen, erzeugen Sie mit _beginthreadex. Ruft ein mit CreateThread erzeugter Thread die CRT auf, kann die CRT den Prozess bei Speichermangel beenden. Sollen viele kurze Aufgaben parallel laufen, verwenden Sie CreateThreadpoolWork aus dem Windows-Threadpool statt immer wieder eigene Threads zu erzeugen.123
Beenden Sie einen Pool-Thread niemals mit ExitThread oder TerminateThread. Ein Pool-Thread ist ein als Callback geliehener Ausführungsplatz: räumen Sie auf und geben Sie ihn zurück.4
Schutz der gemeinsamen Daten und Warten voneinander trennen
Der Standard-Lock innerhalb eines Prozesses ist der SRW-Lock; eine CRITICAL_SECTION wählen Sie, wenn rekursiver Erwerb nötig ist. Ein Mutex für häufigen prozessinternen Ausschluss zahlt die Kosten der Kernelübergänge.5
Einzelne Variablen aktualisieren Sie mit der Interlocked-Familie. volatile allein sichert weder Atomarität noch die nötige Synchronisierung. Die meisten Interlocked-Funktionen tragen eine vollständige Speicherbarriere. Zum Warten verwenden Sie eine Bedingungsvariable oder ein Event plus Wartefunktion, damit Polling mit Sleep weder CPU noch Reaktionsfähigkeit verschwendet.67
Stoppen und Freigabereihenfolge vor dem Start festlegen
Verwenden Sie TerminateThread nicht; entwerfen Sie kooperatives Stoppen mit einem Stopp-Event und WaitForMultipleObjects. Erzwungenes Beenden kann den Zustand von Locks, Heap und DLLs zerstören und ist Ziel der Codeanalyse-Warnung C6258. Gehen Sie nicht allein deshalb zur Freigabe über, weil ein Stopp angefordert wurde; schließen Sie Handles erst, nachdem das Ende des Threads bestätigt ist.89
Erzeugen, synchronisieren oder warten Sie in DllMain nicht auf Threads. Stellen Sie explizite Initialisierungs- und Beendigungsfunktionen außerhalb des Loader-Locks bereit.10
Ist Portabilität gefordert, ist auch C11 eine Option. Mit Stand August 2026 ist <threads.h> ab VS 2022 17.8 nutzbar, <stdatomic.h> ist experimental. Für reinen Windows-Code ist der Win32-API-Ansatz dieses Artikels die Grundlage.11
In der Abbildung kennzeichnet eine durchgezogene Linie eine stets geltende Beziehung und eine gestrichelte Linie eine bedingte (die Bedingungen stehen bei jeder Beziehung auf der Detailseite). Die vollständige Liste der Beziehungen (23 insgesamt, mit Beleg und Sicherheitsgrad) und die Definitionen der wichtigsten Konzepte sind auf der Detailseite der Wissenskarte (auf Japanisch) zusammengestellt. Daten: JSON-LD / Turtle
2. Warum Multithreading schwierig ist — Wettlaufsituationen und Deadlocks
Als Erstes zu unterscheiden sind die Wettlaufsituation, bei der das Ergebnis von der Ausführungsreihenfolge abhängt, und der Deadlock, bei dem ein Kreis von Wartebeziehungen jeden Fortschritt stoppt. Halten Sie diese beiden auseinander, bevor Sie APIs lernen.
Eine Wettlaufsituation (Race Condition) ist ein Fehler, bei dem sich das Ergebnis danach richtet, in welcher Reihenfolge mehrere Threads eine bestimmte Codestelle erreichen.
Nehmen Sie count++ auf einem gemeinsamen Zähler. Obwohl es ein einziger Ausdruck ist, gibt es ohne Synchronisierung keine Garantie, dass „Lesen, Addieren, Zurückschreiben“ unteilbar ausgeführt wird. Lesen zwei Threads denselben Wert, addiert jeder und schreibt jeder zurück, geht eine der Additionen verloren.6 Abbildung 1 zeigt eine Ausführungsreihenfolge, in der der Zähler zweimal von 10 erhöht werden sollte und trotzdem bei 11 landet.
sequenceDiagram
participant A as Thread A
participant M as gemeinsame Variable count
participant B as Thread B
Note over M: count = 10
A->>M: Lesen (10)
B->>M: Lesen (10)
A->>A: lokal addieren (11)
B->>B: lokal addieren (11)
A->>M: Zurückschreiben (11)
B->>M: Zurückschreiben (11)
Note over M: count = 11 nach zwei Erhöhungen<br/>die Addition von Thread A ging verloren
Abbildung 1: Die klassische Wettlaufsituation, bei der ein gemeinsam genutzter Zähler eine Addition verliert. Mischt sich ein anderer Thread zwischen die drei Schritte von count++, überschreibt das spätere Zurückschreiben das andere
Ein Deadlock ist ein Zustand, in dem zwei Threads jeweils auf den Lock warten, den der andere hält, und keiner 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 stoppt beide für immer.
flowchart LR
A["Thread A<br/>hält Lock 1"] -->|"wartet auf Freigabe von Lock 2"| B["Thread B<br/>hält Lock 2"]
B -->|"wartet auf Freigabe von Lock 1"| A
Abbildung 2: Die zirkuläre Wartesituation eines Deadlocks. Sobald die Wartepfeile einen Ring bilden, bleiben alle Threads in diesem Ring für immer stehen
Solche Fehler hängen vom Timing ab. Eine Ausführungsreihenfolge, die auf der Entwicklungsmaschine kaum vorkommt, kann beim Kunden mit anderer Kernzahl und anderer Last wiederholt auftreten. Dass ein Fehler verschwindet, sobald ein Debugger angehängt ist, liegt ebenfalls daran, dass die Beobachtung das Timing verändert.
Deshalb beginnt jedes folgende Prinzip bei „zuerst die Stellen reduzieren, an denen synchronisiert werden muss“, und erst danach „richtig synchronisieren“.
2.1. Was C voraussetzt — die Sprache schützt Sie vor nichts
In C gibt es keinen Sprachmechanismus, der diese Prinzipien erzwingt. Deshalb müssen sie ausdrücklich als Disziplin festgehalten werden.
Die Freigabe bis zu jedem Funktionsausgang entwerfen
Es gibt kein Äquivalent zum RAII von C++, also garantieren Sie selbst die Freigabe von Locks und das CloseHandle von Handles. Nutzen Sie das Muster goto cleanup, das jeden Ausgang einer Funktion an einer Stelle bündelt, oder eine Konvention, Erwerb und Freigabe paarweise zu schreiben. Entscheidend ist eine Struktur, in der ein später ergänzter früher return die Freigabe nicht überspringen kann.
Auch wenn einfaches Lesen und Schreiben atomar ist, braucht es trotzdem Synchronisierung
Datenraces müssen Sie ebenso vermeiden wie in C++. Unter Windows ist ein einfaches Lesen oder Schreiben einer korrekt ausgerichteten 32-Bit-Variablen atomar, aber das allein garantiert weder die Synchronisierung des Zugriffs noch die Reihenfolge umgebender Speicheroperationen. Eine 64-Bit-Variable unter 32-Bit-Windows, eine zusammengesetzte Lese-und-Addiere-Operation und die Konsistenz mehrerer Variablen lassen sich ebenfalls nicht wie ein einfaches Lesen oder Schreiben behandeln.12
Nehmen Sie „es funktioniert zufällig“ nicht als Beleg; machen Sie die nötige Synchronisierung mit einem Lock oder Interlocked explizit. Diese Disziplin bleibt nötig, wenn sich Compiler oder Optimierungsstufe ändern.
Die Eigentümerschaft bis zur Übergabe festhalten
Halten Sie im Funktionskommentar fest, „welcher Thread diesen Puffer schreibt“ und „ab wann er wem gehört“. Die Eigentumsdisziplin ist ebenso wichtig wie die Wahl der Synchronisierungsprimitive. Die später beschriebenen Aufteilungen und Queues sind erst sicher, wenn diese Grenze festliegt.
3. Threads erzeugen — nur _beginthreadex
3.1. Warum CreateThread die falsche Wahl ist
Die native Win32-API ist CreateThread, aber 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 die Ausführung startet.2
Ruft ein mit CreateThread erzeugter Thread eine CRT-Funktion auf, kann die CRT den Prozess bei Speichermangel beenden.1 printf, malloc und strtok sind ebenfalls CRT, deshalb gilt in der Praxis die Grundregel „eigene in C geschriebene Threads verwenden _beginthreadex“.
Die erzeugende Seite ist verantwortlich, bis das Handle erwartet und geschlossen ist
Meiden Sie auch _beginthread (ohne ex). Endet der Thread früh, wird das zurückgegebene Handle ungültig und kann auf einen anderen Thread zeigen. Wählen Sie _beginthreadex, dessen Handle sich an Synchronisierungs-APIs übergeben lässt, und lassen Sie die aufrufende Seite auf das Ende warten und danach CloseHandle aufrufen.13
Der folgende Auszug zeigt Erzeugung und Join. Er setzt voraus, dass die Anwendung die Definition von WorkerContext, die Initialisierung von ctx und die Fehlerbehandlung bereitstellt; es ist kein allein kompilierbares Vollprogramm. Scheitert die Erzeugung, gehen Sie nicht zum Warten über; gelingt sie, halten Sie das vom Worker genutzte ctx gültig, bis das Ende bestätigt ist.
#include <process.h>
static unsigned __stdcall WorkerMain(void* arg)
{
WorkerContext* ctx = (WorkerContext*)arg;
/* ... die Warteschleife aus Kapitel 5 und 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
Wenn Sie „viele kleine Aufgaben einreichen“ oder „kurzlebige Threads immer wieder erzeugen und zerstören“, verwenden Sie den Windows-Threadpool. In der API ab Windows Vista erzeugen Sie mit CreateThreadpoolWork ein Arbeitsobjekt und reichen es mit SubmitThreadpoolWork ein. Die Worker des Pools führen den Callback aus, die Verwaltung der Thread-Anzahl überlassen Sie dem Betriebssystem, und pro Aufgabe entfällt das Erzeugen und Zerstören eines eigenen Threads.3
Den Zustand des geliehenen Threads vor der Rückkehr wiederherstellen
Die offizielle Disziplin lautet: Beenden Sie einen Pool-Thread nicht mit TerminateThread / ExitThread; stellen Sie alles, was der Callback geändert hat — etwa TLS oder die Threadpriorität — vor der Rückkehr wieder her; und halten Sie Wartehandles gültig, bis der Pool sie nicht mehr braucht.4
Die Thread-Anzahl allein begrenzt den Zulauf nicht
Auch wenn der Pool die Thread-Anzahl verwaltet, verhindert das kein Design, in dem unausgeführte Callbacks unbegrenzt auflaufen. In einem residenten Prozess, in dem der Zulauf die Verarbeitung dauerhaft überholt, setzen Sie auf Anwendungsseite eine Zugangsbeschränkung wie ein Semaphor oder eine Queue mit Kapazitätsgrenze.
Legen Sie außerdem fest, ob die einreichende Seite bei vollem Stand wartet oder der Eintrag abgewiesen wird. Das ist Gegendruck (Backpressure), der Überlast nicht in Speicherverbrauch umleitet, und folgt derselben Idee wie der begrenzte Puffer in Kapitel 5.
4. Gemeinsam genutzten veränderlichen Zustand minimieren — Aufteilen, Nur-Lesen, Weitergeben
Bevor Sie eine Synchronisierungsprimitive wählen, fragen Sie, ob sich die Stellen reduzieren lassen, an denen mehrere Threads dieselben veränderlichen Daten anfassen. Die Mittel sind drei: aufteilen, nur lesbar machen und über eine Queue weitergeben.
Pro Thread aufteilen und am Ende zusammenführen
Bei paralleler Aggregation schreibt nicht jeder Thread bei jedem Schritt in einen gemeinsamen Zähler, sondern bildet Zwischensummen in lokalen Variablen oder eigenen Puffern. Führen Sie am Ende genau einmal mit etwas wie InterlockedAdd zusammen, sinken die Schreibzugriffe auf den gemeinsamen Zustand von „bei jeder Iteration“ auf „einmal pro Thread“.
Das senkt Synchronisierungskosten und Konfliktchancen. Die Entscheidung aus Abschnitt 2.1 — „welcher Thread besitzt diesen Puffer“ — wird so direkt zum Aufteilungsentwurf.
Nach abgeschlossener Initialisierung nur noch lesen
Konfiguration und Tabellen, die beim Start aufgebaut und danach nicht mehr geändert werden, dürfen nach Abschluss der Initialisierung von mehreren Threads gelesen werden. Lassen Sie die Grenze nicht unscharf: Schließen Sie die Initialisierung ab, bevor irgendein Thread startet, oder verwenden Sie bei verzögerter Initialisierung InitOnceExecuteOnce.5
Wichtig ist nicht „soll nur gelesen werden“, sondern im Code eindeutig zu machen, wann die Initialisierung endet und ab wann nichts mehr geschrieben wird.
Eine von beiden Seiten angefasste gemeinsame Variable zur Queue-Übergabe machen
Den Datenfluss zwischen Threads leiten Sie über eine Producer/Consumer-Queue, statt eine gemeinsame Variable von beiden Seiten zu bedienen. Für C und Win32 gibt es ein offizielles Implementierungsbeispiel, das einen begrenzten Ringpuffer mit SleepConditionVariableCS kombiniert.7
Eine Kapazitätsobergrenze ergibt zugleich natürlichen Gegendruck: Überholt die Produktion den Konsum, wartet die Erzeugerseite.
5. Wahl der Synchronisierungsobjekte und Lock-Disziplin
Win32 kennt viele Synchronisierungsprimitiven, und eine falsche Wahl kostet Leistung und Korrektheit. Die offizielle Leitlinie steht in einem Bild.5
flowchart TB
S{"Prozessübergreifend<br/>synchronisieren?"} -->|"Ja"| Q2{"Wofür?"}
Q2 -->|"Ausschluss"| MTX["Benannter Mutex"]
Q2 -->|"Begrenzung gleichzeitiger Zugriffe"| SEM["Benanntes Semaphor"]
Q2 -->|"Benachrichtigung über ein Ereignis"| EVT["Benanntes Event"]
S -->|"Nein (prozessintern)"| Q3{"Rekursiver Erwerb<br/>durch denselben Thread nötig?"}
Q3 -->|"Ja"| CS["CRITICAL_SECTION"]
Q3 -->|"Nein"| Q4{"Portabilität im<br/>Vordergrund bei C++-Code?"}
Q4 -->|"Ja"| STD["std::mutex /<br/>std::shared_mutex"]
Q4 -->|"Nein"| SRW["SRW-Lock (Standardwahl)"]
Abbildung 3: Wahl einer Win32-Synchronisierungsprimitive. Der erste Zweig fragt „geht es prozessübergreifend“, und 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, langsam | Prozessübergreifender Ausschluss (benannt) und Kombination mit WaitForMultipleObjects |
| Semaphor | Prozessintern / prozessübergreifend | Kernelobjekt | Begrenzung gleichzeitiger Zugriffe auf einen Ressourcenpool |
| Event | Prozessintern / prozessübergreifend | Kernelobjekt | Benachrichtigung, dass „etwas passiert ist“ (nicht zum Schutz von Daten) |
| Interlocked-Funktionen | Prozessintern (über gemeinsamen Speicher auch prozessübergreifend) | Lockfreie atomare Operationen | Zähler, Flags, Zeigeraustausch6 |
Kernelobjekte sind nicht nur für die prozessübergreifende Nutzung da
Abbildung 3 zeigt den Punkt: Wählen Sie für einen prozessinternen Lock nicht ohne Grund einen Mutex. Events eignen sich für prozessinterne Benachrichtigung, Semaphore für die Begrenzung der Gleichzeitigkeit. Das Stopp-Event in Kapitel 6 ist ebenfalls ein unbenanntes Kernelobjekt.
Umgekehrt beschränkt fehlender Name ein Objekt nicht zwangsläufig auf einen Prozess. Über Handle-Vererbung oder Duplizieren mit DuplicateHandle lässt sich dasselbe Objekt aus mehreren Prozessen nutzen. Benennung ist eines der typischen Mittel, damit ein anderer Prozess dasselbe Objekt erneut öffnen kann.
Interlocked nach Operation, Ausrichtung und Lebensdauer getrennt betrachten
Die Operation auf einer einzelnen Variable unteilbar machen
InterlockedIncrement / InterlockedExchange / InterlockedCompareExchange führen eine Operation auf einer einzelnen Variable unteilbar aus. Sie entsprechen der Klasse Interlocked in der .NET-Edition und std::atomic in der C++-Edition, und die meisten Funktionen tragen außerdem eine vollständige Speicherbarriere.6
Nehmen Sie nicht an, volatile allein ergebe Atomarität oder die nötige Synchronisierung. Sollen mehrere Variablen gemeinsam konsistent bleiben, schützen Sie sie mit einem SRW-Lock oder einer CRITICAL_SECTION.
Die Ausrichtung der Zielvariable einhalten
Das Ziel einer Interlocked-Funktion muss an seiner natürlichen Grenze ausgerichtet sein: 4-Byte-Grenze bei einem 32-Bit-Wert, 8-Byte-Grenze bei einem 64-Bit-Wert. Das Verhalten bei fehlender Ausrichtung ist unvorhersehbar.12
Adressieren Sie kein Feld einer #pragma pack-Struktur und keinen Puffer, der ein Übertragungsformat direkt abbildet; legen Sie Zähler und Flags als gewöhnliche Variablen an, die der Compiler korrekt ausrichtet.
Ein Zeigeraustausch schützt nicht die Lebensdauer der alten Daten
Unteilbar bei InterlockedExchangePointer und ähnlichen Funktionen ist der Zeigeraustausch selbst. Tauscht die schreibende Seite den Zeiger aus und ruft free auf den alten Block auf, unmittelbar nachdem eine lesende Seite den alten Zeiger erhalten hat, greift diese auf bereits freigegebenen Speicher zu.
Austauschen und Zurückgewinnen sind getrennte Probleme. Es braucht ein Verfahren — etwa einen Lock oder eine sichere Referenzzählung —, das den alten Block erst zurücknimmt, wenn die Leser fertig sind. Im Zweifel schützen Sie ihn mit einem SRW-Lock.
Bedingungsvariablen: die Bedingung nach dem Aufwachen prüfen
Eine begrenzte Producer/Consumer-Queue verwendet eine Bedingungsvariable. Initialisieren Sie sie mit InitializeConditionVariable; die Konsumseite wartet mit SleepConditionVariableCS zusammen mit einer CRITICAL_SECTION, die Erzeugerseite weckt mit WakeConditionVariable. Das ist die Form des offiziellen Implementierungsbeispiels.7 In Kombination mit einem SRW-Lock verwenden Sie SleepConditionVariableSRW.
Wichtig ist eine Schleife, die nach dem Aufwachen die Bedingung im Lock erneut prüft und bei falscher Bedingung wieder wartet. Es gibt Scheinwecken ohne Benachrichtigung, und bis zum Aufwachen kann ein anderer Konsument das Element bereits entnommen haben. „Geweckt“ und „die Queue ist nicht leer“ sind nicht dasselbe.
Das ist das Werkzeug, um in C dieselbe Struktur zu bauen wie den Channel aus Abschnitt 4.3 der .NET-Edition und die BlockingQueue aus Kapitel 4 der C++-Edition.
5.1. Lock-Disziplin — drei Prinzipien, die unabhängig von der Primitive gelten
Auch die richtige Wahl der Primitive verhindert Konflikte nicht, wenn die Nutzungsdisziplin fehlt.
1. Die Zuordnung von Daten und Lock festlegen
Ordnen Sie jeder Menge zu schützender veränderlicher Daten genau einen SRW-Lock oder eine CRITICAL_SECTION zu. Nehmen Sie an jeder Stelle, die diese Daten anfasst, denselben Lock.
In C hilft besonders die Praxis, im Header festzuhalten: „diese Struktur wird von g_lockFoo geschützt“. In der Review prüfen Sie diese Zuordnung als Tabelle.
2. Bei gehaltenem Lock keine langwierigen Arbeiten oder externen Aufrufe
Beschränken Sie das Innere eines Locks auf Lesen und Schreiben der geschützten Daten. Datei-I/O, Netzwerkkommunikation und Callback-Aufrufe bei gehaltenem Lock verlängern die Haltezeit. Nimmt die aufgerufene Stelle einen anderen Lock, kann außerdem die zirkuläre Wartesituation aus Abbildung 2 entstehen.
3. Die Erwerbsreihenfolge mehrerer Locks festlegen
Werden zwei oder mehr Locks genommen, definieren Sie eine Lock-Hierarchie, der alle Threads in derselben Reihenfolge folgen. Das Best-Practices-Dokument zu DLLs erklärt ebenfalls, dass eine umgekehrte Erwerbsreihenfolge Deadlocks begünstigt und die Hierarchie konsequent einzuhalten ist.10
6. Das Stoppen entwerfen — ohne TerminateThread
6.1. Was TerminateThread zerstört
TerminateThread beendet den Zielthread, ohne ihm Aufräumen im Usermode zu gestatten. Der gehaltene Zustand wird nicht notwendig sicher aufgeräumt.8
| Zeitpunkt der Beendigung | Mögliche Folge |
|---|---|
| Kritische Sektion gehalten | Der Lock wird nicht freigegeben, andere Threads warten weiter |
| Speicherallokation vom Heap | Der Heap-Lock bleibt gehalten, spätere Allokationen bleiben stehen |
| Globaler Zustand einer DLL wird verändert | Der interne Zustand der DLL wird zerstört |
Offiziell gilt sie als „gefährliche Funktion, die nur im äußersten Notfall verwendet werden sollte“, und die Codeanalyse erkennt sie als Warnung C6258.89
Dass sich bei der Ursachenuntersuchung einer Anwendung, die „gelegentlich als ganzer Prozess hängt“, 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 plus WaitForMultipleObjects
Beim kooperativen Stoppen löscht die stoppende Seite den Thread nicht, sondern fordert den Stopp an und lässt den Worker selbst aufräumen und enden. Die etablierte Form ist ein manuell zurückgesetztes Stopp-Event, auf das der Worker gleichzeitig mit dem Arbeitssignal wartet. Auch das offizielle Material zu C6258 beschreibt, ein Event zu überwachen und selbst zu enden.9
Behandeln Sie Stoppantrag → Aufräumen durch den Worker → Bestätigung des Thread-Endes → Freigabe von Handles und gemeinsamen Ressourcen als getrennte Stufen. Dass das Stopp-Event signalisiert ist, bedeutet noch nicht, dass der Thread geendet hat.
Der folgende Code ist ebenfalls ein erklärender Auszug. Erzeugung und Fehlerprüfung der Events, der gegenseitige Ausschluss der Queue sowie die Implementierungen von ProcessNextItem, LogLastError und Cleanup sind weggelassen. Events und Queue werden während Warten und Verarbeitung nicht zerstört; lesen Sie das Beispiel unter der unten erläuterten Voraussetzung einer Arbeitsbenachrichtigung für einen einzelnen Worker.
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 in einer
Busy-Loop über einer leeren Queue drehen) */
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 und kann der Stopp dort nicht beobachtet werden,
hält das Herunterfahren als Geisel dieses einen Auftrags */
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(); /* eigenes Aufräumen selbst erledigen */
return 0; /* selbst enden */
}
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 gemeinsamer Ressourcen fortfahren */
}
Beim Join erst den Erfolg des Wartens bestätigen, dann weitergehen
StopWorkers prüft zuerst, dass SetEvent gelungen ist. Konnte der Stoppantrag nicht zugestellt werden, darf kein unbegrenztes Join folgen. Auch pro Thread schließt sie nur die Handles, für die WaitForSingleObject WAIT_OBJECT_0 zurückgegeben hat, und meldet bei fehlgeschlagenem Warten nicht „alle sind gestoppt“. Ist der Rückgabewert FALSE, gehen Sie nicht zur Freigabe gemeinsamer Ressourcen über.
Dass auf das Ende einzeln gewartet wird, liegt daran, dass WaitForMultipleObjects höchstens MAXIMUM_WAIT_OBJECTS (64) Handles gleichzeitig erwarten kann. Ein längeres Array kann WAIT_FAILED liefern. Soll nur auf das Ende aller gewartet werden, hat eine Schleife, die einzeln nacheinander wartet, diese Arraylängengrenze nicht.
flowchart TB
OWNER["Stoppende Seite"] -->|"SetEvent(hStopEvent)"| SE["Stopp-Event<br/>(manuell zurückgesetzt: für alle sichtbar)"]
SE --> W1["Worker 1:<br/>wartet mit WaitForMultipleObjects<br/>gleichzeitig auf Stopp und Arbeit"]
SE --> W2["Worker 2:<br/>wartet mit WaitForMultipleObjects<br/>gleichzeitig auf Stopp und Arbeit"]
W1 --> C1["räumt auf und returnt selbst"]
W2 --> C2["räumt auf und returnt selbst"]
C1 --> J["Stoppende Seite wartet auf die Thread-Handles und joint<br/>erst jetzt gilt „gestoppt“"]
C2 --> J
Abbildung 4: Das Stopp-Event-Muster. Ein manuell zurückgesetztes Event für das Stoppen bedeutet, dass ein einziges SetEvent alle wartenden Worker gleichzeitig weckt. Wie er endet, entscheidet jeder Thread selbst, und erst der Abschluss des Joins gilt als gestoppt
Einstellung und Reihenfolge des Stopp-Events einhalten
Machen Sie das Stopp-Event manuell zurückgesetzt, sodass ein SetEvent für alle Worker sichtbar ist. Die Rolle unterscheidet sich von der Arbeitsbenachrichtigung.
Stellen Sie außerdem das Stopp-Event an den Anfang des Wartearrays von WaitForMultipleObjects. Bei einem Warten mit bWaitAll gleich FALSE, wie in diesem Beispiel, hat bei mehreren signalisierten Objekten das Handle mit dem kleineren Index Vorrang. Schließlich schließt die stoppende Seite ein Thread-Handle erst, nachdem der Join darauf bestätigt ist.
Einzelner Worker: per Event wecken und die Queue leeren
Die Form oben — „mit einem automatisch zurückgesetzten Event wecken und die ganze Queue leeren“ — ist für eine Konfiguration mit einem Worker gedacht. Ein automatisch zurückgesetztes Event zählt die Aufgaben nicht, egal wie oft SetEvent aufgerufen wird. Es gibt nur einen signalisierten Zustand, aufeinanderfolgende Signale verschmelzen.
Übernehmen Sie diese Form unverändert für mehrere Worker, kann ein einziger aufwachen und einen Schub Arbeit seriell abarbeiten.
Mehrere Worker: eine Semaphor-Berechtigung einer Aufgabe zuordnen
Teilen sich mehrere Worker die Queue, ersetzen Sie das Arbeitssignal durch ein Semaphor. Die Erzeugerseite erhöht den Zähler mit ReleaseSemaphore(hSem, 1, NULL) für jedes eingereihte Element, die Konsumseite nimmt genau ein Element pro erfolgreichem Warten. Weil erfolgreiches Warten eine Berechtigung verbraucht, bleibt die Entsprechung zur Stückzahl erhalten.
Lassen Sie in diesem Fall die Schleife zum vollständigen Leeren aus dem Beispiel nicht stehen. Leert ein Worker die Queue, obwohl er nur eine Berechtigung verbraucht hat, wachen andere Worker mit den restlichen Berechtigungen zu einer leeren Queue auf, oder das ReleaseSemaphore der Erzeugerseite scheitert an der Überschreitung des Maximums. Voraussetzung ist „eine Berechtigung = eine Aufgabe“.
Den Stopp auch dann beobachtbar machen, wenn eine einzelne Verarbeitung lange dauert
Eine Prüfung nur zwischen den Elementen reicht nicht. Wartet ProcessNextItem intern lange blockierend, übergeben Sie auch dorthin das Stopp-Event und warten Sie überlagert, oder setzen Sie ein endliches Timeout.
Sonst wartet das Herunterfahren unbegrenzt, weil ein einzelnes Element nicht fertig wird. StopAsync in der .NET-Edition und jthread plus Join in der C++-Edition folgen derselben Idee.
Bei blockierendem I/O auch auf der I/O-Seite einen Stopp-Pfad vorsehen
Ein Thread, der auf I/O einer Pipe, eines Sockets oder einer seriellen Schnittstelle wartet, kommt so nicht zurück, um das Stopp-Event zu prüfen. Auch die I/O-Seite braucht einen Weg, der das Warten beendet — überlagertes Warten aus OVERLAPPED plus Event oder Abbrechen des I/O mit CancelIoEx.
Ein konkretes Beispiel behandelt „Fallstricke von Anwendungen mit serieller Kommunikation“.
7. DllMain und der Loader-Lock — das Minenfeld beim Schreiben von DLLs
Gemeinsam genutzte Komponenten in C werden oft zu DLLs, und dort gilt eine eigene Einschränkung: der Loader-Lock. Der OS-Loader ruft DllMain auf, während er den Loader-Lock hält. Die folgenden Aktionen darin führen zu Deadlocks oder Abstürzen.10
- Synchronisierung mit anderen Threads (Lock erwerben, auf das Ende eines Threads warten)
- Aufruf von
LoadLibrary/FreeLibrary, direkt oder indirekt - Threads erzeugen (gefährlich, sobald Synchronisierung dazukommt) oder
ExitThread
Eine explizite Beendigungsfunktion außerhalb von DllMain bereitstellen
„Beim Entladen der DLL in DllMain auf das Ende der Worker warten“ sieht zunächst korrekt aus, ist aber ein klassischer Deadlock. Der endende Thread braucht den Loader-Lock ebenfalls für die Zustellung von DLL_THREAD_DETACH, sodass er und DllMain aufeinander warten.10
Eine DLL mit eigenen Threads stellt Initialisierungs- und Beendigungsfunktionen wie MyLib_Init / MyLib_Shutdown bereit und startet und joint die Threads außerhalb von DllMain. Die aufrufende Seite entlädt erst, nachdem die Beendigungsfunktion den Stopp bestätigt hat. DllMain selbst wird als Stub entworfen, der so leer wie möglich bleibt.10
8. Die Option C11-Threads — der aktuelle Stand
Ohne Abhängigkeit von Win32 Code mit anderen Betriebssystemen zu teilen, sind <threads.h> und <stdatomic.h> aus C11 eine Option. Der MSVC-Stand im August 2026, getrennt betrachtet, ist wie folgt.11
| Funktion | Einordnung in MSVC | Zu prüfende Bedingungen |
|---|---|---|
<threads.h> (thrd_create / mtx_lock / cnd_wait) |
Unterstützt in Visual Studio 2022 17.8 | /std:c11 und ein passendes Windows SDK |
<stdatomic.h> |
experimental | Option /experimental:c11atomics |
Wichtig ist, Threads und atomare Operationen nicht als dieselbe Unterstützungsstufe zu behandeln.
Ist gemeinsamer Code mit Linux eine Anforderung, haben C11-Threads (oder ein pthread-Wrapper) ihren Wert; bei einer reinen Windows-Codebasis ist der Win32-Ansatz dieses Artikels im Hinblick auf Informationsmenge, Erfahrungswerte und Debugbarkeit im Vorteil. Welchen Weg Sie wählen: Die bisherigen Entwurfsprinzipien (Teilen reduzieren, Lock und Daten zuordnen, kooperativ stoppen) ändern sich nicht.
9. Verifikation und Debugging — unter der Annahme vorbereiten, dass es sich nicht reproduziert
Auch wenn gewöhnliche Tests bestehen, folgt daraus kein fehlender Wettlauf. Es bleibt möglich, dass „die problematische Ausführungsreihenfolge zufällig nicht eintrat“. Bereiten Sie in drei Schichten vor: das Design prüfen, Anomalien beobachtbar machen, die Ausführungsreihenfolge unter Last durchschütteln.
Design-Review: Daten, Locks und Stopp-Pfade einander zuordnen
Prüfen Sie als Tabelle die gemeinsam genutzten veränderlichen Daten, die schützenden Locks, die Erwerbsreihenfolge mehrerer Locks und die Worker, die das Stopp-Event erreicht. Das ist die Arbeit, zu sehen, ob die Disziplin aus Abschnitt 5.1 dem tatsächlichen Code entspricht.
Lässt sich diese Zuordnung nicht aufschreiben, kann „es läuft“ nicht als Beleg für einen abgeschlossenen Entwurf dienen.
Beobachtung: Warteorte mit Timeouts, Logs und Dumps festhalten
Machen Sie nicht jedes Warten zu einem bedingungslosen INFINITE, sondern setzen Sie an entscheidenden Stellen Timeouts und protokollieren Sie Zeitüberschreitungen. So wird aus einem Zustand, der nur weiterwartete, ein untersuchbarer Fehlschlag. Ein Timeout ist jedoch keine Bestätigung, dass ein Thread geendet hat, und allein deshalb dürfen gemeinsame Ressourcen nicht freigegeben werden.
Erfassen Sie bei einem Hänger einen Dump, betrachten Sie die Stacks aller Threads und prüfen Sie, ob Lock-Wartezeiten einen Kreis bilden. Für Fehler rund um DLLs nutzen Sie auch Application Verifier, das offiziell empfohlen wird.10 Aufbau von Logs und Dumps behandelt „Design für Logs und Dumps bei Abstürzen von Windows-Apps“.
Lasttest: Ausführungsreihenfolgen erproben, die auf der Entwicklungsmaschine selten auftreten
Stresstests — langes Laufen mit mehr Threads als Kernen, Randomisieren der Verarbeitungsreihenfolge, Einfügen künstlicher Verzögerungen — erhöhen die Chance, dass ein Fehler auftritt. Prüfen Sie auch die Kombination aus optimiertem Release-Build und hoher Last.
Lasttest ersetzt das Design-Review nicht. Kombinieren Sie die drei Schichten, damit sich die Ursache verfolgen lässt, wenn ein Fehler reproduziert wird.
10. Zusammenfassung — Checkliste der C-Edition
- Wird jeder Thread mit
_beginthreadexerzeugt (mischen sich keineCreateThread/_beginthreaddarunter)? - Werden Thread-Handles erst gejoint (
WaitForSingleObject) und dann mitCloseHandlegeschlossen? - Werden für kurzlebige Aufgaben nicht wahllos eigene Threads erzeugt (ließen sie sich der Threadpool-API übergeben)?
- Erfolgt prozessinterner Ausschluss über SRW-Lock / CRITICAL_SECTION (statt eines missbrauchten Mutex)?
- Werden gemeinsame Zähler und Flags mit Interlocked-Funktionen statt mit bloßem
volatileaktualisiert? - Ist keine
Sleep-Polling-Schleife mehr übrig (wurde sie durch Bedingungsvariable oder Event-Warten ersetzt)? - Gibt es an keiner einzigen Stelle
TerminateThread(erzwungenes Beenden eines anderen Threads)? Enden Worker über einreturnaus der Thread-Funktion statt über einen Aufruf vonExitThread(sodass die CRT-Nachbereitung korrekt über_endthreadexläuft)? - Verfügt jeder Worker über einen Stopp-Pfad aus Stopp-Event plus
WaitForMultipleObjects, und lassen sich auch Threads wecken, die in blockierendem I/O stecken? - Ist die Freigabe von Locks und Handles auf jedem Rückgabepfad garantiert (die
goto cleanup-Disziplin)? - Werden in
DllMainkeine Threads erzeugt, synchronisiert oder auf ihr Ende gewartet?
Anstelle sprachlicher Unterstützung ist die Qualität von Multithreading in C genau das, was API-Wahl und Disziplin daraus machen. _beginthreadex, SRW-Lock, Interlocked und das Stopp-Event — machen Sie dieses Vierergespann zum Standard, und auch in C gelingt ein Entwurf mit Abstand zum „gelegentlichen Hängen“.
Verwandte Artikel
- Praktische Multithreading-Best-Practices: .NET-Edition
- Praktische Multithreading-Best-Practices: C++-Edition
- Praktische Multithreading-Best-Practices: Java-Edition
- Fallstricke bei gemeinsam genutztem Speicher und praktische Best Practices
- Warum Sie unter Windows Event-Warten Sleep(1) vorziehen sollten
- Fallstricke von Anwendungen mit serieller Kommunikation
- Design für Logs und Dumps bei Abstürzen von Windows-Apps
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 durch TerminateThread oder leckende Locks sowie die technische Beratung zum Hinzufügen von Threads zu Legacy-C-Code.
- Technische Beratung und Design-Review
- Fehleruntersuchung und Ursachenanalyse
- Windows-Anwendungsentwicklung
- Kontakt
Referenzlinks
-
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 bei Speichermangel beenden kann, wenn ein mit CreateThread erzeugter Thread die CRT aufruft. ↩ ↩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
-
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
-
Microsoft Learn, Thread Pools. Dazu, dass sich der Threadpool für Anwendungen eignet, die eine große Zahl kurzer Aufgaben asynchron ausführen oder 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
-
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
-
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 Erhöhung durch zwei Threads eine Erhöhung verlieren kann; zur Funktionsfamilie InterlockedExchange / InterlockedCompareExchange; 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
-
Microsoft Learn, Using Condition Variables. Zum Implementierungsbeispiel einer Producer/Consumer-Queue über einen durch CRITICAL_SECTION geschützten begrenzten Ringpuffer; zur Struktur, bei der InitializeConditionVariable eine Bedingungsvariable erzeugt, der Konsument mit SleepConditionVariableCS wartet und die Gegenseite mit WakeConditionVariable geweckt wird; sowie zur Unterstützung von Bedingungsvariablen ab Windows Vista. ↩ ↩2 ↩3
-
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 ↩3
-
Microsoft Learn, Warning C6258. Dazu, dass die Codeanalyse-Warnung C6258 die Verwendung von TerminateThread erkennt; dazu, dass mit TerminateThread keine angemessene Thread-Bereinigung möglich ist; sowie dazu, dass als korrekte Beendigung 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
-
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 die Zustellung von DLL_THREAD_DETACH 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 ↩6
-
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
-
Microsoft Learn, Interlocked Variable Access. Dazu, dass ein einfaches Lesen oder Schreiben einer korrekt ausgerichteten 32-Bit-Variablen atomar ist, die Synchronisierung (Ordnung) des Zugriffs aber nicht garantiert wird; dazu, dass ein einfaches Lesen oder Schreiben einer 64-Bit-Variablen 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
-
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 und 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. ↩
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Praktische Multithreading-Best-Practices: C++-Edition — Fehler mit RAII und jthread strukturell ausschließen
In C++ wird eine Datenrace zu undefiniertem Verhalten. Der Artikel ordnet die Falle des std::thread-Destruktors, das Stoppen mit jthread ...
Praktische Best Practices für Multithreading: .NET-Edition — Was Sie entscheiden sollten, bevor Sie weitere Threads hinzufügen
Eine praxisnahe Übersicht der Entwurfsregeln, die verhindern, dass Multithreading-Code in .NET/C# gelegentlich abstürzt oder hängen bleib...
Scheinwecken — Warum Bedingungsvariablen „ohne Benachrichtigung“ aufwachen und wie man unter Windows richtig wartet
Das wait einer Bedingungsvariable kann auch ohne Benachrichtigung zurückkehren (Scheinwecken). Der Artikel erklärt anhand der Windows-Ums...
Praktische Best Practices für Multithreading: Java-Edition — Konventionen im Zeitalter der virtuellen Threads
In Java erzeugt man Threads nicht selbst, sondern setzt auf ExecutorService und virtuelle Threads. Der Artikel ordnet synchronized und Re...
Apps, die nach dem Fortsetzen aus dem Schlaf kaputtgehen — Energieereignisse und Geschäftsanwendungen, die das Fortsetzen überstehen
Sie klappen den Laptop auf, und die Verbindungen der Geschäftsanwendung sind tot — die Ursache ist ein Entwurf, der Schlaf nicht vorsieht...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Fehleranalyse und Langzeitprobleme
Sporadische Fehler, Kommunikationsdiagnose, Langzeitabstürze und Tests von Fehlerpfaden.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Technische Beratung und Design-Review
Klärung von Änderungsstrategie, Entwurf und Umgang mit bestehenden Assets.
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 braucht, bevor der Thread startet. Die offizielle Dokumentation hält ausdrücklich fest, dass die CRT den Prozess bei Speichermangel beenden kann, wenn ein mit CreateThread erzeugter Thread eine CRT-Funktion aufruft. In der Praxis ruft ein Thread 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 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 die Codeanalyse erkennt es als Warnung C6258. Das korrekte Stoppen ist kooperatives Stoppen: Sie erzeugen ein Stopp-Event, jeder Thread überwacht es 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 bleiben und nur bei Konflikten in ein Kernel-Warten fallen — erheblich schneller, und 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 threads.h und stdatomic.h aus C11 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 einen SRW-Lock 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.