Win32-Thread-Pool-API — Nebenläufigkeit ohne eigene Threads, mit CreateThreadpoolWork
· Aktualisiert am: · Go Komura · Windows, Multithreading, C++, Windows-Entwicklung, Win32-API, Leistungsverbesserung
Änderungsverlauf (Erstfassung, veröffentlicht am 22. Aug 2026)
- Erstveröffentlichung
Diesen Artikel zitieren(DOI (registriertes Archiv): 10.5281/zenodo.22176798)
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). Win32-Thread-Pool-API — Nebenläufigkeit ohne eigene Threads, mit CreateThreadpoolWork. KomuraSoft LLC. https://comcomponent.com/de/blog/win32-thread-pool-api/
- DOI (registriertes Archiv)
- 10.5281/zenodo.22176798
- DOI (zuletzt registrierte Version)
- 10.5281/zenodo.22176799
„Ein CreateThread pro Client.“ „Einen für den Timer.“ „Einen für das Warten auf ein Ereignis.“ In nativem Windows-Code neigt man dazu, für kleine Aufgaben Threads zu vermehren. Jeder Thread verbraucht einen Stack und ein Kernelobjekt; Anlegen und Zerstören haben ebenfalls Kosten.
Was diese Diskrepanz auffängt, ist die betriebssystemeigene Win32-Thread-Pool-API. Die App übergibt die gewünschte Verarbeitung als Callback und überlässt die Verwaltung der Worker-Threads dem Betriebssystem. „Keine Threads anlegen“ heißt nicht, dass Threads überflüssig werden. Es heißt, dass die App nicht für jede Aufgabe selbst anlegt und zerstört.1
Dieser Artikel richtet sich an Entwickler, die Windows-Apps, Dienste und DLLs in C/C++ schreiben. Die Reihenfolge ist Wahl des Werkzeugs → Auswahl des Objekts → Implementierung von work → Hinweise zu Callbacks und Herunterfahren. Gegenstand ist die in Windows Vista neu gestaltete Familie CreateThreadpoolWork. Der Code ist ein Auszug, der das Gerüst der Nutzung zeigt; app-spezifische Teile wie die Warteschlange sind weggelassen.
1. Zuerst das Fazit
Wenn Sie viele kurzlebige Aufgaben verarbeiten, verwalten Sie Aufgaben statt Threads. Wann eine Aufgabe stoppt und wann sie freigegeben werden darf, entwirft jedoch die App.
Die Entscheidung dreht sich um drei Achsen.
- Das Werkzeug wählen. Reichen Standard-C++ oder .NET, nutzen Sie die. Diese API ist dran, wenn Sie unter Win32 Timer, Warten und I/O-Fertigstellung vereinheitlichen oder Pools trennen wollen.
- Arbeit in Aufgabeneinheiten übergeben. Nutzen Sie work, timer, wait und io der neuen API. Arbeit, die thread-spezifischen Zustand braucht — Threadpriorität, COM-STA —, bleibt auf einem eigenen Thread.
- Das Herunterfahren als Teil des Satzes bauen. Die Grundform ist „Übergeben stoppen → auf Abschluss warten → schließen“. Im Callback nicht lange blockieren, nicht synchron auf Arbeit desselben Pools warten, den Threadzustand zurücksetzen.
Wer zuerst etwas zum Laufen bringen will, beginnt mit dem work-Beispiel in Kapitel 4 und prüft danach die Callback-Disziplin in Kapitel 5. Das gebündelte Herunterfahren mehrerer Objekte behandelt Kapitel 6, das Entladen einer DLL Kapitel 7.
2. Wahl der Mittel — Pool, eigener Thread, Standardbibliothek
2.1 Ein Pool passt zu kurzlebiger, zahlreicher, wartezentrierter Arbeit
Ein Thread-Pool ist eine Menge von Worker-Threads, die das Betriebssystem verwaltet. Die Worker führen Callbacks nacheinander aus, und das Betriebssystem passt die Zahl an die Last an. Indem Sie das Anlegen und Abreißen eigener Threads abgeben, sinken Verwaltungscode und die Kosten von Erzeugung und Zerstörung.1
Die offizielle Dokumentation nennt als Kandidaten Apps, die viele kleine Arbeitselemente parallel ausgeben, Apps, die häufig kurzlebige Threads anlegen und zerstören, Apps, die unabhängige Arbeit im Hintergrund parallel verarbeiten, und Apps, die eigene Threads nur zum Warten auf Kernelobjekte oder Ereignisse halten. Suche, Netz-I/O und das Aufräumen von Threads, die nur warten, sind typisch.1
Andererseits bleibt Arbeit, die eine Änderung der Threadpriorität braucht, COM-STA verlangt oder die gesamte Lebensdauer des Prozesses weiterläuft, auf einem eigenen Thread. Das Kriterium ist, ob es reicht, dass die Verarbeitung läuft, oder ob der Thread selbst eine „Persönlichkeit“ braucht.
flowchart TB
accTitle: Wahl zwischen eigenem Thread und Pool
accDescr: Zuerst prüfen, ob eine Thread-Persönlichkeit wie Priorität oder STA nötig ist und ob die Arbeit lange läuft; nur kurzlebige, zahlreiche oder warteartige Arbeit, die zu keinem von beiden passt, kommt auf den Thread-Pool
q1{"Persönlichkeit nötig, z. B. Priorität oder STA?"} -->|"Ja"| ded["Auf eigenem Thread halten"]
q1 -->|"Nein"| q2{"Läuft lange weiter?"}
q2 -->|"Ja"| ded
q2 -->|"Nein"| pool["Auf den Thread-Pool legen"]
pool -.-> ex["Kurzlebige Arbeit, Warten, Timer, I/O-Fertigstellung"]
Abbildung 1: Auf den Pool gehört nur Arbeit, die „keine Persönlichkeit braucht und schnell endet“. Alles andere bleibt wie bisher auf einem eigenen Thread.
2.2 Zuerst fragen, ob Standard-C++ oder .NET reicht
Reicht die Granularität mit C++ std::async oder std::thread, ist die Standardbibliothek der erste Kandidat. Sie ist portabel, und das Verhalten von std::async und future lässt sich am Standard festmachen.2
Gründe, den Win32-Thread-Pool direkt zu nutzen, sind Anforderungen wie ein einheitlicher Callback-Mechanismus, der timer, wait und io einschließt; getrennte Pools oder Threadzahlen je Arbeitsart; keine eigenen Threads in einer DLL oder COM-Komponente. Trennen Sie die Frage, ob Sie lediglich parallelisieren wollen, von der, ob Sie Windows-spezifische Steuerung brauchen.
flowchart TB
accTitle: Entscheidung, auf welcher Schicht geschrieben wird
accDescr: Reichen async oder thread von Standard-C++, nutzt man sie; den Win32-Thread-Pool direkt nutzen, wenn Timer, Warten und I/O-Fertigstellung vereinheitlicht, Pools getrennt oder Threadzahlen gesteuert werden sollen, oder eigene Threads in einer DLL oder COM-Komponente vermieden werden sollen
q1{"Reichen die Werkzeuge von Standard-C++?"} -->|"Ja"| std["std::async / std::thread"]
q1 -->|"Nein"| q2{"Was wird gebraucht?"}
q2 -->|"Vereinheitlichung von timer, wait, io"| tp["Win32-Thread-Pool"]
q2 -->|"Pooltrennung / Zahlensteuerung"| tp
q2 -->|"Keine eigenen Threads in einer DLL"| tp
Abbildung 2: Im Zweifel zuerst die Standardbibliothek. Diese API ist dran, wenn eine Anforderung auftaucht, die sich dort nicht ausdrücken lässt.
In .NET übernehmen ThreadPool und Task dieselbe Rolle, und I/O-Fertigstellung ist an IOCP gebunden. Eine ausführliche Erklärung des Zusammenhangs steht im Artikel zu IOCP und dem .NET-Thread-Pool. Wo die Werkzeuge der Schicht darüber reichen, muss man nicht zur Win32-API hinabsteigen.
2.3 In neuem Code die neue API ab Vista nutzen
Es gibt zwei Generationen der Win32-Thread-Pool-API: die alte API, die seit Windows 2000 weiterläuft, etwa QueueUserWorkItem und RegisterWaitForSingleObject, und die in Windows Vista vollständig neu gestaltete Familie CreateThreadpoolWork.
Die neue API vereinheitlichte die Arten von Worker-Threads und stellte eine einzige Timer-Warteschlange, eigene persistente Threads, mehrere unabhängige Pools im Prozess und Cleanup-Gruppen bereit. Die offizielle Dokumentation nennt als Vorteile auch Einfachheit, Zuverlässigkeit, Leistung und Flexibilität.13
Die alte API hat außerdem die strukturelle Grenze, dass Arbeit nach dem Einreihen nicht abgebrochen werden kann. Nutzen Sie in neuem Code die neue API. Beim Durchsehen bestehenden Codes ist die folgende Zuordnung der Ausgangspunkt.3
flowchart TB
accTitle: Zuordnung zwischen alter Thread-Pool-API und neuer API
accDescr: QueueUserWorkItem der alten API wird durch das work-Objekt der neuen API ersetzt, Timer-Warteschlangen durch timer, registriertes Warten durch wait, BindIoCompletionCallback durch io
o1["QueueUserWorkItem"] --> n1["work"]
o2["Timer-Warteschlange"] --> n2["timer"]
o5["Registriertes Warten"] --> n3["wait"]
o4["BindIoCompletionCallback"] --> n4["io"]
Abbildung 3: Das Migrationsziel von der alten API ist eins zu eins festgelegt. Eine Inventur bestehenden Codes kann bei dieser Zuordnung beginnen.
3. Aufbau — vier Objekte mit unterschiedlichen Auslösebedingungen
3.1 Nach dem Auslöser des Callbacks wählen
Im Zentrum der neuen API stehen die folgenden vier Arten. Wählen Sie nicht nur nach „was verarbeitet werden soll“, sondern danach, wodurch der Callback ausgelöst werden soll.4
| Objekt | Erstellungsfunktion | Auslösebedingung des Callbacks |
|---|---|---|
| work | CreateThreadpoolWork |
Wenn mit SubmitThreadpoolWork übergeben |
| timer | CreateThreadpoolTimer |
Wenn der angegebene Zeitpunkt oder die Periode eintrifft |
| wait | CreateThreadpoolWait |
Wenn ein Kernelobjekt signalisiert wird |
| io | CreateThreadpoolIo |
Wenn asynchrones I/O am zugeordneten Handle fertig ist |
Die Auslösebedingungen unterscheiden sich, aber die Worker desselben Pools führen aus. Statt periodische Verarbeitung, Ereignisreaktion und I/O-Fertigstellung jeweils auf einem eigenen Thread zu schreiben, können Sie sie auf einem gemeinsamen Callback-Mechanismus ausrichten.
flowchart TB
accTitle: Auslösebedingungen und Callback-Ausführung trennen
accDescr: Die Worker des Pools führen Callbacks aus, die Objekte mit Auslösebedingungen bereitgemacht haben; ein fertiger Worker wird auch für den nächsten Callback genutzt, sodass die App nicht je Aufgabe einen Thread anlegt
app["Die App gibt Aufgabe und Auslösebedingung vor"] --> obj["Das Objekt wartet auf die Bedingung"]
obj --> ready["Der Callback wird ausführbereit"]
ready --> workers["Die Worker des Pools führen aus"]
workers --> done["Verarbeitung beenden und Worker zurückgeben"]
done --> next["Auch für den nächsten Callback wiederverwendet"]
Abbildung 4: Die Trennung zwischen dem Mechanismus, der auf den Auslöser wartet, und den Workern, die verarbeiten, verringert die Threadverwaltung je Aufgabe.
3.2 Auch „Threads, die nur schlafen und warten“ lassen sich ersetzen
Der Nutzen des Pools beschränkt sich nicht auf CPU-Arbeit. Timer werden in einer einzigen Timer-Warteschlange zusammengeführt, das Warten auf mehrere Handles auf wenige Warte-Threads.1
Gibt es etwa fünf Threads, die nur schlafen, damit sie „laufen, wenn das Ereignis signalisiert wird“, kann man sie durch fünf wait-Objekte ersetzen. Statt je einen schlafenden Thread zu halten, führen Sie die Verarbeitung beim Signal als Callback aus.
flowchart TB
accTitle: Ersetzen von Threads, die nur warten, durch wait-Objekte
accDescr: Threads, die je Ereignis einzeln schlafen und nur warten, werden durch wait-Objekte auf die Warte-Threads des Pools zusammengezogen; der Callback läuft nur beim Signal
old2["5 Threads, die nur warten, schlafen einzeln"] -.-> waste["Verbraucht 5 Stacks und 5 Threads"]
new2["5 wait-Objekte"] --> agg["Auf die Warte-Threads des Pools zusammengezogen"]
agg --> cb2["Callback nur beim Signal"]
Abbildung 5: „Threads, die nur schlafen und warten“, lassen sich durch wait-Objekte beseitigen. Das ist der naheliegende erste Schritt einer Pool-Migration.
4. Grundlagen der Implementierung — work anlegen, übergeben, sicher herunterfahren
4.1 Zuerst ein Rundgang von der Erstellung bis zum Herunterfahren
Der Rundgang nutzt das grundlegendste Objekt, work. CreateThreadpoolWork bindet Callback und context, SubmitThreadpoolWork fordert die Ausführung an. Ist das dritte Argument NULL, wird der Standard-Pool des Prozesses genutzt. Für viele Zwecke reicht dieser Standard-Pool.56
Das Folgende ist ein Auszug, der das Verfahren zeigt, kein vollständiges Programm, das sich so kompilieren lässt. WORK_QUEUE, ITEM, Enqueue, Dequeue und ProcessItem stehen für Verarbeitung auf der App-Seite. Gegenseitigen Ausschluss der Warteschlange, das Anhalten der Übergeberseite und die Fehlerbehandlung implementieren Sie getrennt. Schlägt das Anlegen des work fehl, gehen Sie nicht zu den folgenden Schritten Übergeben, Warten und Freigeben weiter.
VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE instance,
PVOID context, PTP_WORK work)
{
// Der context wird bei der Erstellung festgehalten. Daten je Element kommen über eine synchronisierte Warteschlange
WORK_QUEUE* queue = (WORK_QUEUE*)context;
ITEM* item = Dequeue(queue); // Ein Element unter gegenseitigem Ausschluss entnehmen
ProcessItem(item);
}
// 1) Erstellen (Callback und den gemeinsamen Kontext, also die Warteschlange, binden)
PTP_WORK work = CreateThreadpoolWork(WorkCallback, &queue, NULL);
if (!work) { /* Fehlerbehandlung mit GetLastError */ }
// 2) Pro eingereihtem Element einmal übergeben (Elementzahl und Übergabezahl gleich halten)
Enqueue(&queue, item);
SubmitThreadpoolWork(work);
// 3) Die Übergeberseite anhalten, dann auf Abschluss warten (TRUE versucht zusätzlich, noch nicht gestartete Callbacks abzubrechen)
WaitForThreadpoolWorkCallbacks(work, FALSE);
// 4) Schließen
CloseThreadpoolWork(work);
flowchart TB
accTitle: Lebenszyklus eines work-Objekts
accDescr: Mit CreateThreadpoolWork anlegen; Übergeben mit SubmitThreadpoolWork führt Callbacks parallel aus. Beim Herunterfahren zuerst neues Übergeben stoppen, mit WaitForThreadpoolWorkCallbacks auf den Abschluss aller Callbacks warten, dann mit CloseThreadpoolWork schließen
c["Mit CreateThreadpoolWork anlegen"] --> s["Mit SubmitThreadpoolWork übergeben (mehrfach möglich)"]
s --> run["Callbacks laufen parallel"]
run --> stop3["Neues Übergeben stoppen"]
stop3 --> w["Mit WaitForThreadpoolWorkCallbacks auf Abschluss warten"]
w --> cl["Mit CloseThreadpoolWork schließen"]
Abbildung 6: Die Reihenfolge beim Herunterfahren ist „Übergeben stoppen → auf Abschluss warten → schließen“. Wird ein Schritt übersprungen, entstehen Zugriff nach Freigabe oder ein Wettlauf.
4.2 Ein work lässt sich wiederverwenden, der context ändert sich aber nicht je Übergabe
Dasselbe work-Objekt kann mehrfach übergeben werden, auch bevor der vorherige Callback fertig ist. Jede Übergabe führt den Callback aus, und mehrere Instanzen laufen parallel. Die tatsächlich genutzte Threadzahl kann der Pool aus Effizienzgründen anpassen.7
Zu unterscheiden sind hier das work und die Daten jedes Arbeitselements. Der context, der an den Callback übergeben wird, ist bei der Erstellung fest. Sollen N Elemente mit unterschiedlichen Daten über ein work verarbeitet werden, machen Sie wie im Beispiel oben eine synchronisierte Warteschlange zum context. Übergeben Sie einmal je eingereihtem Element, und lassen Sie den Callback ein Element aus der Warteschlange nehmen. Ein Entwurf, der je Element ein work-Objekt anlegt, ist ebenfalls in Ordnung.5
flowchart TB
accTitle: Daten je Element über einen festen context empfangen
accDescr: Der context wird bei der work-Erstellung auf eine gemeinsame Warteschlange festgelegt; die Übergeberseite übergibt einmal je eingereihtem Element, und jeder parallel laufende Callback nimmt unter gegenseitigem Ausschluss je ein Element aus der Warteschlange
create["context bei der work-Erstellung festlegen"] -.-> queue["Gemeinsame Warteschlange mit gegenseitigem Ausschluss"]
item["Ein Element einreihen"] --> queue
item --> submit["Einmal je Element übergeben"]
submit --> callbacks["Callbacks laufen parallel"]
callbacks --> dequeue["Je ein Element entnehmen"]
queue --> dequeue
dequeue --> process["Das entnommene Element verarbeiten"]
Abbildung 7: Fest ist der context, der auf die Warteschlange zeigt; Daten je Element kommen über die synchronisierte Warteschlange.
4.3 Beim Herunterfahren das Übergeben stoppen, bevor gewartet wird
Die sichere Reihenfolge ist „neues Übergeben stoppen → mit WaitForThreadpoolWorkCallbacks auf Abschluss warten → mit CloseThreadpoolWork schließen“. Speicher, auf den der Callback verweist, darf ebenfalls nicht freigegeben werden, bevor der Abschluss feststeht. Wird das Verweisziel freigegeben, während noch laufende oder wartende Verarbeitung bleibt, folgt Zugriff nach Freigabe.6
„Ich habe auf Abschluss gewartet, also ist es sicher“ reicht nicht. Kann ein anderer Thread noch SubmitThreadpoolWork aufrufen, entsteht nach dem Warten ein Wettlauf zwischen Übergabe und Close. Damit das Warten eine Zäsur im Herunterfahren ist, muss die Übergeberseite zuvor angehalten sein.
Ist das zweite Argument von WaitForThreadpoolWorkCallbacks FALSE, wird auf Abschluss gewartet; ist es TRUE, wird zusätzlich der Abbruch noch nicht gestarteter Callbacks angefordert. Das heißt nicht, dass bereits laufende Verarbeitung zurückgelassen werden darf. Wie mehrere Objekte gemeinsam heruntergefahren werden, behandelt Kapitel 6.
5. Callback-Disziplin — einen geliehenen Thread weder besetzen noch verunreinigen
5.1 Lange Arbeit erklären und den Rückgabewert prüfen
Der Pool passt die Threadzahl unter der Annahme an, dass Callbacks zügig zurückkehren. Setzen Sie lange Verarbeitung oder langes Warten fort, ohne etwas mitzuteilen, verzögert sich die Ausführung anderer Callbacks. Kann die Arbeit lange dauern, teilen Sie das mit CallbackMayRunLong mit oder verlagern Sie sie auf einen eigenen Thread.8
Nur CallbackMayRunLong aufzurufen reicht jedoch nicht. Die Funktion gibt FALSE zurück, wenn sie keinen Worker für andere Callbacks bereitstellen kann. Ignorieren Sie den Rückgabewert nicht und blockieren Sie nicht weiter; neigen Sie in diesem Fall dazu, den Pool nicht zu verstopfen: die Verarbeitung teilen, auf einen eigenen Thread verlagern und so weiter.8
flowchart TB
accTitle: Umgang mit einem langlebigen Callback festlegen
accDescr: Arbeit, die lange Verarbeitung oder langes Warten braucht, wird auf einen eigenen Thread verlagert oder dem Pool mit CallbackMayRunLong gemeldet; kommt FALSE zurück, weil kein anderer Worker bereitgestellt werden konnte, vermeidet man das Blockieren durch Teilen oder Verlagern auf einen eigenen Thread
long["Lange Verarbeitung oder langes Warten nötig"] --> choice{"Wo verarbeiten?"}
choice -->|"Eigenen Thread"| dedicated["Auf einen eigenen Thread verlagern"]
choice -->|"Auf dem Pool"| notify["Mit CallbackMayRunLong mitteilen"]
notify --> result{"Wurde ein anderer Worker gesichert?"}
result -->|"TRUE"| run["Die lange Verarbeitung ausführen"]
result -->|"FALSE"| split["Teilen oder auf einen eigenen Thread verlagern"]
Abbildung 8: Die Meldung langer Verarbeitung ist erst vollständig, wenn der Rückgabewert geprüft und die nächste Handlung festgelegt ist.
5.2 Innerhalb eines Workers nicht synchron auf Arbeit desselben Pools warten
Ein Entwurf, in dem Callback A demselben Pool Arbeit B übergibt und mit WaitForThreadpoolWorkCallbacks oder ähnlichem auf deren Abschluss wartet, braucht Vorsicht. Sind alle Worker damit beschäftigt, „auf die Arbeit eines anderen Workers zu warten“, bleibt kein freier Worker, der B ausführen könnte, und es entsteht ein Deadlock durch Poolverhungern.
Die Abhilfe ist nicht, einen Worker zum Warten vorzuhalten, sondern in eine Fortsetzung zu wechseln, in der der Abschluss-Callback von B die nächste Arbeit übergibt. Schreiben Sie Abhängigkeiten nicht als Warten, das einen Worker blockiert.
flowchart TB
accTitle: Aufbau eines Deadlocks durch Poolverhungern
accDescr: Warten alle Worker-Threads synchron auf den Abschluss anderer Arbeit, die demselben Pool übergeben wurde, gibt es keinen freien Worker, der diese Arbeit ausführen könnte, und alle warten ewig im Deadlock
w1["Worker 1: wartet auf Abschluss von Arbeit X"] --> q["Arbeit X und Y warten auf Ausführung"]
w2["Worker 2: wartet auf Abschluss von Arbeit Y"] --> q
q -.-> none["Kein freier Worker, der ausführen könnte"]
none -.-> dead["Alle warten ewig (Deadlock durch Verhungern)"]
Abbildung 9: Wartet ein Worker synchron auf einen Worker, bleibt niemand, der die erwartete Arbeit ausführt.
5.3 Den Threadzustand zurücksetzen, bevor zurückgekehrt wird
Ein Worker-Thread wird auch für den nächsten, unabhängigen Callback genutzt. Eine geänderte Priorität belassen, COM-Initialisierungszustand hinterlassen, einen Wert in TLS lassen oder eine Sperre nicht lösen — das überträgt sich auf die nächste Arbeit. Sie dürfen nicht annehmen, dass die dem Pool übergebene Funktion und die von ihr aufgerufene Verarbeitung auf einem eigenen Thread laufen.9
Es gibt auch APIs, die die Aufräumarbeit an das Ende des Callbacks koppeln. LeaveCriticalSectionWhenCallbackReturns etwa beauftragt den Pool, den Critical Section zu lösen, nachdem der Callback zurückgekehrt ist.4
flowchart TB
accTitle: Threadzustand nicht in den nächsten Callback mitnehmen
accDescr: Weil ein anderer Callback denselben Worker wiederverwendet, verunreinigt das Zurückkehren mit hinterlassenem Prioritäts-, COM-, TLS- oder Sperrzustand die nächste Arbeit; Aufräumen und Zurücksetzen verhindert diese Mitnahme
first["Callback A leiht sich einen Worker"] --> cleanup{"Vor dem Zurückkehren aufgeräumt?"}
cleanup -->|"Nein"| dirty["Mit hinterlassenem Zustand wiederverwendet"]
dirty --> impact["Betrifft das unabhängige B"]
cleanup -->|"Ja"| clean["Worker im ursprünglichen Zustand zurückgegeben"]
clean --> next["Der nächste Callback B nutzt ihn"]
Abbildung 10: Der Thread ist geliehen. Verantwortlich sind Sie nicht nur für das Ergebnis der Verarbeitung, sondern auch für den Threadzustand beim Zurückgeben.
5.4 Unbehandelte Ausnahmen nicht aus dem Worker entweichen lassen
Eine unbehandelte Ausnahme auf einem Worker-Thread kann den ganzen Prozess mitreißen. Wie bei der Threadfunktion eines eigenen Threads gilt die Linie, Ausnahmen am Eingang des Callbacks zu fangen und zu protokollieren. Dass der Pool die Ausführung übernimmt, macht den Umgang mit Fehlern in der Arbeit nicht überflüssig.
6. Die Konfiguration trennen — eigene Pools und Cleanup-Gruppen
6.1 Einen eigenen Pool nutzen, um Arbeitsarten zu isolieren
Reicht der Standard-Pool nicht mehr, können Sie mit CreateThreadpool einen unabhängigen Pool anlegen. SetThreadpoolThreadMaximum und SetThreadpoolThreadMinimum setzen Ober- und Untergrenze der Worker-Threadzahl.10
Der typische Zweck ist Isolation. Damit „Batchverarbeitung, die langsam sein darf“ nicht die Worker von „Arbeit, die sofort antworten soll“ aufbraucht, trennen Sie die Pools und geben jedem ein Budget an Threads. Es geht nicht einfach um mehr Threads, sondern darum, welche Arbeit welche Worker nutzt.
6.2 Ausführungsziel und Aufräumarbeit über die Callback-Umgebung binden
Was festlegt, auf welchem Pool ausgeführt wird, ist TP_CALLBACK_ENVIRON. Sie initialisieren diese Callback-Umgebung, geben den Pool mit SetThreadpoolCallbackPool an und übergeben sie an CreateThreadpoolWork und verwandte Funktionen. Das dritte Argument, das im Beispiel von Kapitel 4 NULL war, ist der Platz für die Umgebung.5
Über dieselbe Umgebung lässt sich auch eine Cleanup-Gruppe anbinden. Der eigene Pool ist das Ausführungsziel, die Cleanup-Gruppe die Einheit der Aufräumarbeit. Wenn Sie diese beiden Rollen getrennt sehen, lässt sich die Konfiguration leichter verfolgen.
flowchart TB
accTitle: Binden der Konfiguration über die Callback-Umgebung
accDescr: Die Callback-Umgebung zeigt auf einen eigenen Pool und eine Cleanup-Gruppe; work- und timer-Objekte, die mit dieser Umgebung angelegt werden, laufen auf diesem Pool, und die Sammeloperation der Cleanup-Gruppe bündelt Abschlusswarten und Freigabe
env["Callback-Umgebung (TP_CALLBACK_ENVIRON)"] --> cp["Eigener Pool (steuert die Threadzahl)"]
env --> cg["Cleanup-Gruppe"]
env --> obj["Beim Anlegen von work / timer / wait / io übergeben"]
cg -.-> close["Abschlusswarten und Freigabe bündeln"]
Abbildung 11: Die Callback-Umgebung ist der Mechanismus, der beim Anlegen des Objekts injiziert, „auf welchem Pool es läuft und wer aufräumt“.
6.3 Abschlusswarten und Freigabe mehrerer Objekte bündeln
Wenn work, timer und andere Objekte in einem Modul zunehmen, wird das Herunterfahren eine Liste von „auf jedes warten, jedes schließen“. Legen Sie mit CreateThreadpoolCleanupGroup eine Gruppe an und machen Sie die über die Callback-Umgebung erstellten Objekte zu Mitgliedern, bündelt ein einziges CloseThreadpoolCleanupGroupMembers Abschlusswarten und Freigabe aller Mitglieder.46
Auch hier ist das Ziel, keinen laufenden Callback zurückzulassen. Wählen Sie zwischen dem einzelnen Herunterfahren von work wie in Kapitel 4 und dem Herunterfahren auf Gruppenebene nach der Zahl der verwalteten Objekte.
7. Nutzung aus einer DLL — nicht vor den Callbacks entladen
7.1 Auf Abschluss in einer expliziten Herunterfahrfunktion warten, nicht in DllMain
Das Gefährlichste in einer DLL ist, dass die DLL entladen wird, während ihr Callback-Code noch ausgeführt werden kann. Läuft entladener Code, gibt es eine Zugriffsverletzung.
Die Grundregel ist, in der expliziten Herunterfahrfunktion der DLL das Übergeben zu stoppen, auf den Abschluss der Callbacks zu warten und die Objekte zu schließen, bevor entladen wird. Ob mit einzelnen Wartefunktionen oder mit der Cleanup-Gruppe aus Kapitel 6: Diese Abschlussprüfung darf nicht entfallen.6
Dieses Warten dürfen Sie nicht innerhalb von DllMain ausführen. Wegen der Beziehung zur Ladersperre entsteht ein anderer Deadlock. Details stehen im Artikel zu DllMain und der Ladersperre.
7.2 FreeLibraryWhenCallbackReturns mit einer Referenz vor dem Übergeben koppeln
Für die Lage „dieser Callback ist die letzte Arbeit, nach der Rückkehr will ich die DLL-Referenz loslassen“ gibt es FreeLibraryWhenCallbackReturns.4
Diese API allein verhindert jedoch kein Entladen, bevor der Callback beginnt. Was sie tut, ist, eine Modulreferenz loszulassen, wenn der laufende Callback zurückkehrt. Nutzen Sie sie als Paar: Nehmen Sie vor dem Übergeben mit GetModuleHandleEx eine Modulreferenz für diese Verarbeitung, und lassen Sie sie aus dem Callback mit dieser API los.
flowchart TB
accTitle: Die DLL in einer Herunterfahrfunktion schließen gegenüber eine Referenz aus dem Callback zurückgeben
accDescr: Die Grundform ist eine Herunterfahrfunktion außerhalb von DllMain, die Übergeben stoppt, auf Abschluss wartet und freigibt, bevor die DLL entladen wird; in einem Entwurf, in dem der letzte Callback die Referenz zurückgibt, die Referenz vor dem Übergeben mit GetModuleHandleEx nehmen und mit der Freigabe nach der Rückkehr über FreeLibraryWhenCallbackReturns koppeln
shutdown["Explizite Herunterfahrfunktion"] --> stop["Übergeben stoppen, warten, freigeben"]
stop --> unload["Danach die DLL entladen"]
note["In DllMain nicht warten"] -.-> shutdown
acquire["Vor dem Übergeben eine Modulreferenz nehmen"] --> submit["Den Callback übergeben"]
submit --> callback["Die Freigabe im Callback vormerken"]
api["FreeLibraryWhenCallbackReturns"] -.-> callback
callback --> returned["Nach der Rückkehr eine Referenz loslassen"]
Abbildung 12: Verwechseln Sie das Abschlusswarten in der Herunterfahrfunktion nicht mit dem Loslassen der für den Callback genommenen Referenz; in beiden Fällen entwerfen Sie zuerst die Lebensdauer der DLL.
8. Fazit — zuerst work, und zusammen mit dem Herunterfahren ersetzen
Der Win32-Thread-Pool ist die Grundlage, die Nebenläufigkeit in nativem Code von „Threads anlegen“ zu „Aufgaben als Callback übergeben“ verschiebt. Kurzlebige Aufgaben und Threads, die nur warten, lassen sich aufräumen; die Threadverwaltung übernimmt das Betriebssystem.
Die Einführung darf schrittweise erfolgen. Zuerst mit work Erstellung, Übergeben, Stoppen des Übergebens, Warten auf Abschluss und Freigabe zu einem Satz machen. Als Nächstes Threads, die nur warten, durch wait ersetzen und Threads, die nur Timer sind, durch timer. io-Integration und Pooltrennung erwägen Sie, wenn sie nötig werden.
flowchart TB
accTitle: Bestehenden Code schrittweise auf den Thread-Pool umstellen
accDescr: Die bestehenden selbst verwalteten Threads durchsehen, Arbeit belassen, die zur Standardbibliothek oder zu einem eigenen Thread passt, poolgeeignete Arbeit einschließlich Stoppen des Übergebens und Warten auf Abschluss auf work umstellen, Warten schrittweise durch wait und Timer durch timer ersetzen
inventory["Bestehende selbst verwaltete Threads durchsehen"] --> choose{"Passt die Arbeit zum Pool?"}
choose -->|"Nein"| keep["Standardbibliothek oder eigenen Thread wählen"]
choose -->|"Ja"| work["Auf work umstellen, Herunterfahren als Satz"]
work --> more["Warten nach wait, Timer nach timer"]
more --> optional["Bei Bedarf I/O-Integration, Pooltrennung"]
Abbildung 13: Nicht nur Threads reduzieren, sondern in jeder Stufe das Herunterfahren mitliefern — das ist die Grundlage einer schrittweisen Migration.
Bis zum Schluss gelten drei Disziplinen: nicht lange blockieren, nicht synchron auf denselben Pool warten, den Threadzustand nicht verunreinigen. In einer DLL kommt hinzu, den Wettlauf mit dem Entladen zu verhindern. Was Standard-C++ oder .NET abdeckt, überlassen Sie denen. Nutzen Sie diese API dort, wo Windows-spezifische Integration oder Steuerung nötig ist.
Verwandte Artikel
- Die Tiefen von Windows-I/O (Teil 3) — I/O-Completion-Ports (IOCP) und der .NET-Thread-Pool: Der Keller unter async/await
- Praktische Multithreading-Best-Practices: C++-Edition
- Praktische Multithreading-Best-Practices: C-Edition
- Scheinwecken — Warum Bedingungsvariablen „ohne Benachrichtigung“ aufwachen und wie Sie unter Windows richtig warten
- DllMain und die Ladersperre — Der wahre Grund, warum man sagt, in der DLL-Initialisierung nichts zu tun
- Warum Sie unter Windows Ereigniswarten gegenüber Sleep(1) bevorzugen sollten
Zugehörige Beratungsfelder
KomuraSoft LLC übernimmt den Migrationsentwurf von nativem Code, dessen Threads sich vermehrt haben, auf den Thread-Pool, Design-Reviews nebenläufiger Verarbeitung in C++-Apps und -DLLs sowie die Ursachenuntersuchung von Hängern und Abstürzen durch Poolverhungern oder Callbacks. Sie können uns gern beginnend bei einer Inventur bestehenden Codes konsultieren.
- Windows-Anwendungsentwicklung
- Technische Beratung und Design-Review
- Fehleruntersuchung und Ursachenanalyse
- Kontakt
Quellen
-
Microsoft Learn, Thread Pools. Dazu, dass ein Thread-Pool eine Sammlung von Worker-Threads ist, die asynchrone Callbacks im Auftrag der App effizient ausführen; zu den App-Typen, für die er geeignet ist (viele kleine Arbeitselemente parallel ausgeben, häufig kurzlebige Threads anlegen und zerstören, unabhängige Arbeit parallel verarbeiten, ausschließliches Warten auf Kernelobjekte und so weiter); sowie zum vollständigen Neuentwurf in Vista (Vereinheitlichung der Worker-Thread-Arten, eine einzige Timer-Warteschlange, eigene persistente Threads, Cleanup-Gruppen, mehrere Pools im Prozess und die neue API). ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, <future>. Dazu, dass asynchrone Ausführung pro Aufgabe über std::async und future als Standardbibliothek bereitsteht, sodass Nebenläufigkeit geschrieben werden kann, ohne Threads direkt zu verwalten. ↩
-
Microsoft Learn, Thread Pooling. Zur Struktur der älteren Thread-Pool-API (QueueUserWorkItem, Timer-Warteschlangen, registriertes Warten, BindIoCompletionCallback); dazu, dass Arbeit nach dem Einreihen nicht abgebrochen werden kann; sowie dazu, dass die in Vista eingeführte neue Thread-Pool-API ausdrücklich als einfacher und in Zuverlässigkeit, Leistung und Flexibilität überlegen bezeichnet wird. ↩ ↩2
-
Microsoft Learn, threadpoolapiset.h header. Zur Funktionsliste, die die vier Objekt-Erstellungsfunktionen CreateThreadpoolWork, CreateThreadpoolTimer, CreateThreadpoolWait und CreateThreadpoolIo umfasst; Cleanup-Gruppen (CreateThreadpoolCleanupGroup); sowie Aufräumarbeit, gekoppelt an den Callback-Abschluss (LeaveCriticalSectionWhenCallbackReturns, FreeLibraryWhenCallbackReturns und so weiter). ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, CreateThreadpoolWork function (threadpoolapiset.h). Zum Anlegen eines work-Objekts aus einer Callback-Funktion und einem Kontextzeiger; sowie dazu, dass das dritte Argument, TP_CALLBACK_ENVIRON, die Ausführungsumgebung des Callbacks angeben kann (den Pool, zu dem er gehört, und so weiter), wobei NULL bedeutet, dass er in der Standardumgebung läuft. ↩ ↩2 ↩3
-
Microsoft Learn, Using the Thread Pool Functions. Zum Grundverfahren Anlegen mit CreateThreadpoolWork, Übergeben mit SubmitThreadpoolWork, Warten auf Abschluss mit WaitForThreadpoolWorkCallbacks und Schließen mit CloseThreadpoolWork; sowie zu einem Konfigurationsbeispiel, das einen eigenen Pool mit einer Callback-Umgebung und einer Cleanup-Gruppe kombiniert. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, SubmitThreadpoolWork function (threadpoolapiset.h). Dazu, dass dasselbe work-Objekt mehrfach übergeben werden kann, ohne auf den Abschluss eines vorausgehenden Callbacks zu warten, sodass Callbacks parallel laufen; sowie dazu, dass der Pool die Threadzahl zur Effizienz anpassen (drosseln) kann. ↩
-
Microsoft Learn, CallbackMayRunLong function (threadpoolapiset.h). Zum Mitteilen an den Pool, dass der aktuelle Callback lange laufen kann, sodass der Pool das als Grundlage nutzen kann, ob er Threads für andere Callbacks sichert; sowie dazu, wo möglich für einen langlebigen Callback einen eigenen Thread zu erwägen. ↩ ↩2
-
Microsoft Learn, Thread Pooling. Dazu, dass einem Thread-Pool übergebene Arbeitselemente und die von ihnen aufgerufenen Funktionen thread-pool-sicher sein müssen; dazu, dass nicht angenommen werden darf, der ausführende Thread sei ein eigener, persistenter Thread; sowie dazu, TLS und asynchrone Aufrufe zu vermeiden, die einen persistenten Thread brauchen. ↩
-
Microsoft Learn, SetThreadpoolThreadMaximum function (threadpoolapiset.h). Dazu, dass für einen mit CreateThreadpool angelegten Pool eine Obergrenze der Worker-Threadzahl gesetzt werden kann (die Untergrenze ist SetThreadpoolThreadMinimum). ↩
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
DllMain und die Ladersperre — Der wahre Grund, warum man sagt, in der DLL-Initialisierung nichts zu tun
Warum Sie aus DllMain weder LoadLibrary aufrufen noch mit Threads synchronisieren dürfen. Anhand von Primärquellen erklärt dieser Artikel...
Was nach dem Tod des Elternprozesses übrig bleibt — Kindprozesse in einem Job Object halten
Warum SDK-Helfer eine beendete UI überleben und Kamera oder COM-Port behalten. Kindprozesslebensdauer mit Job Object, KillOnJobClose und ...
Named Pipes in der Praxis — Windows-Standard-IPC von der Auslegung bis zur Sicherheit
Ein Praxisleitfaden zu Named Pipes, der Standard-Prozesskommunikation unter Windows. Der Artikel ordnet anhand von Primärquellen die Wahl...
Was „Keine Rückmeldung“ wirklich ist — Wie Windows entscheidet, dass eine App hängt, und Entwürfe, die nicht hängen
„Keine Rückmeldung“ unter Windows ist ein Mechanismus, bei dem das Betriebssystem urteilt, dass ein Fenster 5 Sekunden lang keine Nachric...
Time Travel Debugging — Langlaufende Fehler, die sich nicht reproduzieren, aufzeichnen und zurückspulen
Ein Fehler, der nur einmal im Monat auftritt, hinterlässt im Absturz-Dump nur das Ergebnis. Mit Time Travel Debugging (TTD) in WinDbg zei...
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.
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.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Was ist an einem Thread-Pool besser, als mit CreateThread eigene Threads anzulegen?
- Die Effizienz, wenn viele kurzlebige Aufgaben anfallen, und weniger Code zur Threadverwaltung. Das Anlegen und Zerstören eines Threads hat Kosten, die man nicht ignorieren kann. Eine App, die „CreateThread für jede Aufgabe und nach Abschluss zerstören“ wiederholt, oder viele Threads hält, die nur schlafen, um auf ein Ereignis zu warten, kann durch den Wechsel auf einen Pool Threadzahl und Kontextwechsel senken. Die offizielle Dokumentation nennt als Pool-Kandidaten Apps, die viele kleine Arbeitselemente parallel ausgeben, Apps, die viele kurzlebige Threads anlegen, und Apps, die Threads ausschließlich zum Warten auf Kernelobjekte haben. Umgekehrt sollte Arbeit, bei der „der Thread selbst eine Persönlichkeit braucht“ — Prioritätswechsel, COM-STA, lang laufende Spezialverarbeitung — weiterhin auf einem eigenen Thread bleiben.
- Worin unterscheidet sich das von älteren Thread-Pool-Funktionen wie QueueUserWorkItem?
- Der Thread-Pool wurde in Windows Vista vollständig neu gestaltet. Die heutige Familie threadpoolapiset (CreateThreadpoolWork und verwandte Funktionen) ist die neue API; QueueUserWorkItem, RegisterWaitForSingleObject und Ähnliches sind die alte (Legacy-)API. Die neue API vereinheitlicht die Arten von Worker-Threads, erlaubt mehrere unabhängige Pools im Prozess und stellt Mechanismen bereit wie die Sammelfreigabe über eine Cleanup-Gruppe sowie das Lösen einer Sperre oder das Entladen einer DLL, gekoppelt an den Callback-Abschluss. Die offizielle Dokumentation bezeichnet die neue API als einfacher und in Zuverlässigkeit, Leistung und Flexibilität überlegen. Die alte API hat auch strukturelle Grenzen, etwa „es gibt keinen Weg, Arbeit nach dem Einreihen abzubrechen“. In neuem Code sollten Sie daher die neue API verwenden.
- Gibt es Dinge, die man in einem Callback nicht tun darf?
- Im Wesentlichen drei. Erstens: langes Blockieren oder lange Verarbeitung bei den Voreinstellungen. Der Pool passt die Threadzahl unter der Annahme an, dass Callbacks zügig zurückkehren; für lange Arbeit erklären Sie das mit CallbackMayRunLong oder nutzen Sie einen eigenen Thread. Zweitens: synchrones Warten auf den Abschluss anderer Arbeit, die demselben Pool übergeben wurde. Sind alle Worker damit beschäftigt, „auf einen anderen Worker zu warten“, entsteht ein Deadlock durch Poolverhungern. Drittens: Abhängigkeit von der Persönlichkeit des Threads. Worker-Threads werden zwischen Callbacks geteilt; eine geänderte Threadpriorität oder ein geänderter COM-Initialisierungszustand beim Zurückkehren, oder Zustand, der in TLS bleibt, verunreinigt den nächsten Callback. Für die Aufräumarbeit am Ende (eine Sperre lösen oder eine DLL entladen) gibt es eigene Mechanismen wie LeaveCriticalSectionWhenCallbackReturns und FreeLibraryWhenCallbackReturns.
- Worauf muss man achten, wenn man den Thread-Pool in einer DLL nutzt?
- Die größte Gefahr ist: „Die DLL wird entladen, während ein Callback noch laufen kann.“ Läuft der Callback nach dem Entladen, gibt es eine Zugriffsverletzung. Die DLL muss in ihrer Herunterfahrverarbeitung den Abschluss der von ihr ausgegebenen Callbacks zuverlässig abwarten — mit einer Wartefunktion wie WaitForThreadpoolWorkCallbacks oder mit CloseThreadpoolCleanupGroupMembers auf einer Cleanup-Gruppe — und erst dann die Objekte schließen. Dieses Warten innerhalb von DllMain kann jedoch durch die Ladersperre in einen Deadlock führen; die Regel lautet, es in einer expliziten Herunterfahrfunktion zu tun, nicht in DllMain. Für den Fall, dass der Callback selbst „die letzte Arbeit“ ist und die DLL freigeben will, gibt es die eigene API FreeLibraryWhenCallbackReturns.
- Gibt es, wo C++ std::async und der .NET-ThreadPool existieren, noch Anlässe, diese API direkt zu nutzen?
- Ja. Das Kriterium ist, ob das Werkzeug auf dieser Schicht reicht. Deckt die Granularität der Nebenläufigkeit, die Sie in C++ brauchen, std::async oder std::thread ab, ist die Standardbibliothek schon aus Portabilitätsgründen der erste Kandidat. Andererseits: Timer, Warten auf Kernelobjekte und asynchrone I/O-Fertigstellungen in einem Callback-Mechanismus vereinheitlichen; Pools trennen und Threadzahlen je Arbeitsart steuern; in einer DLL oder COM-Komponente keine eigenen Threads halten — das sind Anforderungen, die der Win32-Thread-Pool abdeckt. Das Verhältnis zum ThreadPool und IOCP von .NET behandelt ein verwandter Artikel. Solange Sie nativ schreiben, ist die Kenntnis dieses Mechanismus eine Schicht darunter nicht verschwendet.
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.