Threadpools

Ein Threadpool ist eine Sammlung von Arbeitsthreads, die asynchrone Rückrufe im Auftrag der Anwendung effizient ausführen. Der Threadpool wird in erster Linie verwendet, um die Anzahl der Anwendungsthreads zu reduzieren und die Verwaltung der Arbeitsthreads bereitzustellen. Anwendungen können Arbeitsaufgaben in eine Warteschlange stellen, Arbeit mit Handles verknüpfen, auf die gewartet werden kann, automatisch basierend auf einem Timer in eine Warteschlange stellen und mit E/A binden.

Architektur des Threadpools

Die folgenden Anwendungen können von der Verwendung eines Threadpools profitieren:

  • Eine Anwendung, die hochgradig parallel ist und eine große Anzahl kleiner Arbeitsaufgaben asynchron verteilen kann (z. B. verteilte Indexsuche oder Netzwerk-E/A).
  • Eine Anwendung, die eine große Anzahl von Threads erstellt und löscht, die jeweils nur kurze Zeit aktiv sind. Die Verwendung des Threadpools kann die Komplexität der Threadverwaltung und den Aufwand für die Threaderstellung und -löschung reduzieren.
  • Eine Anwendung, die unabhängige Arbeitsaufgaben im Hintergrund und parallel verarbeitet (z. B. mehrere Registerkarten laden).
  • Eine Anwendung, die ausschließlich auf Kernel-Objekte warten oder eingehende Ereignisse auf einem Objekt blockieren muss. Durch die Verwendung des Threadpools kann die Komplexität der Threadverwaltung reduziert und die Leistung durch die Reduzierung der Anzahl der Kontextwechsel erhöht werden.
  • Eine Anwendung, die benutzerdefinierte Wartethreads zum Warten auf Ereignisse erstellt.

Der ursprüngliche Threadpool wurde in Windows Vista vollständig neu gestaltet. Der neue Threadpool wurde optimiert, da er einen einheitlichen Workerthread-Typ bereitstellt (der sowohl E/A als auch Nicht-E/A unterstützt), keinen Timerthread verwendet, eine einzelne Timerwarteschlange bereitstellt und einen dedizierten permanenten Thread bietet. Er bietet außerdem Bereinigungsgruppen, höhere Leistung, mehrere Pools pro Prozess, die unabhängig geplant werden, und eine neue Threadpool-API.

Die Threadpool-Architektur besteht aus Folgendem:

  • Worker-Threads, die die Callback-Funktionen ausführen
  • Wartethreads, die auf mehrere Wartehandles warten
  • Eine Arbeitswarteschlange
  • Ein Standardthreadpool für jeden Prozess
  • Eine Worker-Factory, die die Worker-Threads verwaltet

Bewährte Methoden

Die neue Threadpool-API bietet mehr Flexibilität und Kontrolle als die ursprüngliche Threadpool-API. Es gibt jedoch ein paar kleine, aber wichtige Unterschiede. In der ursprünglichen API war die Wartezurücksetzung automatisch; in der neuen API muss die Wartezeit jedes Mal explizit zurückgesetzt werden. Die ursprüngliche API führte die Impersonation automatisch aus und übertrug den Sicherheitskontext des aufrufenden Prozesses auf den Thread. Mit der neuen API muss die Anwendung explizit den Sicherheitskontext festlegen.

Es folgen bewährte Methoden bei der Verwendung eines Threadpools:

  • Die Threads eines Prozesses teilen den Threadpool. Ein einzelner Arbeitsthread kann mehrere Rückruffunktionen gleichzeitig ausführen. Diese Worker-Threads werden vom Threadpool verwaltet. Beenden Sie daher keinen Thread aus dem Threadpool, indem Sie TerminateThread für den Thread oder ExitThread aus einer Rückruffunktion aufrufen.

  • Eine E/A-Anforderung kann auf einem beliebigen Thread im Threadpool ausgeführt werden. Das Abbrechen von E/A-Vorgängen in einem Threadpoolthread erfordert eine Synchronisierung, da die cancel-Funktion möglicherweise in einem anderen Thread ausgeführt wird als dem, der die E/A-Anforderung verarbeitet, was zum Abbruch eines unbekannten Vorgangs führen kann. Um dies zu vermeiden, stellen Sie immer OVERLAPPED-Struktur bereit, mit der eine E/A-Anforderung beim Aufrufen von CancelIoEx für asynchrone E/A initiiert wurde, oder verwenden Sie ihre eigene Synchronisierung, um sicherzustellen, dass keine andere E/A im Zielthread gestartet werden kann, bevor Sie entweder die Funktion CancelSynchronousIo oder die Funktion CancelIoEx aufrufen.

  • Bereinigen Sie alle in der Rückruffunktion erstellten Ressourcen, bevor Sie die Funktion verlassen. Dazu gehören TLS, Sicherheitskontexte, Threadpriorität und COM-Registrierung. Rückruffunktionen müssen auch den Threadzustand wiederherstellen, bevor sie zurückgegeben werden.

  • Halten Sie die Warte-Handles und die damit verbundenen Objekte aktiv, bis der Threadpool signalisiert hat, dass er mit dem Handle fertig ist.

  • Markieren Sie alle Threads, die auf langwierige Vorgänge warten (z. B. E/A-Löschvorgänge oder Ressourcenbereinigung), damit der Threadpool neue Threads zuordnen kann, anstatt auf diese zu warten.

  • Bevor Sie eine DLL entladen, die den Threadpool verwendet, brechen Sie alle Arbeitselemente, E/A-Vorgänge, Wartevorgänge und Timer ab und warten Sie, bis die gerade ausgeführten Rückruffunktionen abgeschlossen sind.

  • Vermeiden Sie Deadlocks, indem Sie Abhängigkeiten zwischen Arbeitsaufgaben und zwischen Rückrufen beseitigen, indem Sie sicherstellen, dass ein Rückruf nicht auf sich selbst wartet, um abgeschlossen zu werden, und indem Sie die Threadpriorität beibehalten.

  • Fügen Sie nicht zu viele Elemente zu schnell in einen Prozess mit anderen Komponenten ein, die den Standardthreadpool verwenden. Pro Prozess gibt es einen Standardthreadpool, einschließlich Svchost.exe. Standardmäßig verfügt jeder Threadpool über maximal 500 Arbeitsthreads. Der Threadpool versucht, mehr Arbeitsthreads zu erstellen, wenn die Anzahl der Arbeitsthreads im Zustand „Bereit/Wird ausgeführt“ kleiner als die Anzahl der Prozessoren sein muss.

  • Vermeiden Sie das COM-Einzelthread-Apartmentmodell, da es nicht mit dem Threadpool kompatibel ist. STA erstellt den Threadzustand, der sich auf die nächste Arbeitsaufgabe für den Thread auswirken kann. STA ist in der Regel langlebig und hat Threadaffinität, was das Gegenteil des Threadpools ist.

  • Erstellen Sie einen neuen Threadpool, um die Threadpriorität und -isolation zu steuern, benutzerdefinierte Merkmale zu erstellen und möglicherweise die Reaktionsfähigkeit zu verbessern. Zusätzliche Threadpools erfordern jedoch mehr Systemressourcen (Threads, Kernelspeicher). Zu viele Pools erhöhen das Potenzial für CPU-Konflikte.

  • Verwenden Sie nach Möglichkeit ein wartefähiges Objekt anstelle eines APC-basierten Mechanismus, um einen Thread im Threadpool zu signalisieren. APCs funktionieren nicht genauso gut mit Threadpoolthreads wie andere Signalmechanismen, da das System die Lebensdauer von Threadpoolthreads steuert, sodass es möglich ist, dass ein Thread beendet wird, bevor die Benachrichtigung übermittelt wird.

  • Verwenden Sie die Threadpool-Debuggererweiterung !tp. Dieser Befehl hat folgende Verwendung:

    • Pooladresskennzeichnungen
    • obj AdresseFlags
    • tqueue AdresseKennzeichen
    • Warteadresse
    • Arbeitsadresse

    Für Pool, Waiter und Worker: wenn die Adresse null ist, werden alle Objekte gelöscht. Bei Waiter und Worker wird durch Weglassen der Adresse ein Dump des aktuellen Threads erstellt. Die folgenden Flags sind definiert: 0x1 (einzeilige Ausgabe), 0x2 (Mitglieder ausgeben) und 0x4 (Pool-Arbeitswarteschlange ausgeben).

Note

Moderne Alternativen: Bei C++/WinRT-Anwendungen kann über Coroutinen auf den Windows-Threadpool zugegriffen werden — verwenden Sie co_await winrt::resume_background(), um Arbeit an den Threadpool zu übergeben, und co_await winrt::resume_foreground(dispatcher), um zum UI-Thread zurückzukehren. Dadurch wird eine strukturierte Parallelität ohne manuelle Rückrufverwaltung bereitgestellt. Für standardmäßigen C++-Code bietet std::async mit std::launch::async-Richtlinie portable aufgabenbasierte Parallelität, obwohl es den Windows-Threadpool nicht direkt verwendet.

Threadpool-API

Verwenden der Threadpoolfunktionen