Win32-Thread-Pool-API — Nebenläufigkeit ohne eigene Threads, mit CreateThreadpoolWork

· Aktualisiert am: · · 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.

Wahl zwischen eigenem Thread und PoolZuerst 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-PoolJaNeinJaNeinPersönlichkeit nötig, z. B. Priorität oder STA?Auf eigenem Thread haltenLäuft lange weiter?Auf den Thread-Pool legenKurzlebige 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.

Entscheidung, auf welcher Schicht geschrieben wirdReichen 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 sollenJaNeinVereinheitlichung von timer, wait, ioPooltrennung / ZahlensteuerungKeine eigenen Threads in einer DLLReichen die Werkzeuge von Standard-C++?std::async / std::threadWas wird gebraucht?Win32-Thread-Pool

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

Zuordnung zwischen alter Thread-Pool-API und neuer APIQueueUserWorkItem der alten API wird durch das work-Objekt der neuen API ersetzt, Timer-Warteschlangen durch timer, registriertes Warten durch wait, BindIoCompletionCallback durch ioQueueUserWorkItemworkTimer-WarteschlangetimerRegistriertes WartenwaitBindIoCompletionCallbackio

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.

Auslösebedingungen und Callback-Ausführung trennenDie 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 anlegtDie App gibt Aufgabe und Auslösebedingung vorDas Objekt wartet auf die BedingungDer Callback wird ausführbereitDie Worker des Pools führen ausVerarbeitung beenden und Worker zurückgebenAuch 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.

Ersetzen von Threads, die nur warten, durch wait-ObjekteThreads, 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 Signal5 Threads, die nur warten, schlafen einzelnVerbraucht 5 Stacks und 5 Threads5 wait-ObjekteAuf die Warte-Threads des Pools zusammengezogenCallback 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);
Lebenszyklus eines work-ObjektsMit 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ßenMit CreateThreadpoolWork anlegenMit SubmitThreadpoolWork übergeben (mehrfach möglich)Callbacks laufen parallelNeues Übergeben stoppenMit WaitForThreadpoolWorkCallbacks auf Abschluss wartenMit 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

Daten je Element über einen festen context empfangenDer 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 Warteschlangecontext bei der work-Erstellung festlegenGemeinsame Warteschlange mit gegenseitigem AusschlussEin Element einreihenEinmal je Element übergebenCallbacks laufen parallelJe ein Element entnehmenDas 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

Umgang mit einem langlebigen Callback festlegenArbeit, 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 ThreadEigenen ThreadAuf dem PoolTRUEFALSELange Verarbeitung oder langes Warten nötigWo verarbeiten?Auf einen eigenen Thread verlagernMit CallbackMayRunLong mitteilenWurde ein anderer Worker gesichert?Die lange Verarbeitung ausführenTeilen 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.

Aufbau eines Deadlocks durch PoolverhungernWarten 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 DeadlockWorker 1: wartet auf Abschluss von Arbeit XArbeit X und Y warten auf AusführungWorker 2: wartet auf Abschluss von Arbeit YKein freier Worker, der ausführen könnteAlle 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

Threadzustand nicht in den nächsten Callback mitnehmenWeil 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 MitnahmeNeinJaCallback A leiht sich einen WorkerVor dem Zurückkehren aufgeräumt?Mit hinterlassenem Zustand wiederverwendetBetrifft das unabhängige BWorker im ursprünglichen Zustand zurückgegebenDer 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.

Binden der Konfiguration über die Callback-UmgebungDie 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 FreigabeCallback-Umgebung (TP_CALLBACK_ENVIRON)Eigener Pool (steuert die Threadzahl)Cleanup-GruppeBeim Anlegen von work / timer / wait / io übergebenAbschlusswarten 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.

Die DLL in einer Herunterfahrfunktion schließen gegenüber eine Referenz aus dem Callback zurückgebenDie 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 koppelnExplizite HerunterfahrfunktionÜbergeben stoppen, warten, freigebenDanach die DLL entladenIn DllMain nicht wartenVor dem Übergeben eine Modulreferenz nehmenDen Callback übergebenDie Freigabe im Callback vormerkenFreeLibraryWhenCallbackReturnsNach 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.

Bestehenden Code schrittweise auf den Thread-Pool umstellenDie 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 ersetzenNeinJaBestehende selbst verwaltete Threads durchsehenPasst die Arbeit zum Pool?Standardbibliothek oder eigenen Thread wählenAuf work umstellen, Herunterfahren als SatzWarten nach wait, Timer nach timerBei 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

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.

Quellen

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

  2. 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. ↩

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

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

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

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

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

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

  9. 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. ↩

  10. 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). ↩

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.

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.

Zurück zum Blog