Pools de threads

Un pool de threads est un ensemble de threads de travail qui exécutent efficacement des rappels asynchrones pour le compte de l’application. Le pool de threads est principalement utilisé pour réduire le nombre de threads d’application et fournir la gestion des threads de travail. Les applications peuvent mettre en file d’attente des éléments de travail, associer des travaux à des handles pouvant être mis en attente, mettre automatiquement en file d’attente en fonction d’un retardateur et établir une liaison avec des E/S.

Architecture d’un pool de threads

Les applications suivantes peuvent tirer parti d’un pool de threads :

  • Une application qui est hautement parallèle et qui peut distribuer un grand nombre de petits éléments de travail de manière asynchrone (par exemple, recherche d’index distribué ou E/S réseau).
  • Une application qui crée et détruit un grand nombre de threads qui s’exécutent chacun pendant une courte durée. L’utilisation du pool de threads peut réduire la complexité de la gestion des threads et la surcharge qu’implique la création et la destruction des threads.
  • Une application qui traite des éléments de travail indépendants en arrière-plan et en parallèle (par exemple, le chargement de plusieurs onglets).
  • Une application qui doit effectuer une attente exclusive sur des objets du noyau ou bloquer des événements entrants sur un objet. L’utilisation du pool de threads peut réduire la complexité de la gestion des threads et améliorer les performances en réduisant le nombre de changements de contexte.
  • Une application qui crée des threads de waiter personnalisés pour attendre les événements.

Le pool de threads d’origine a été complètement remanié dans Windows Vista. Le nouveau pool de threads est amélioré, car il fournit un seul type de thread de travail (il prend en charge les E/S et les non-E/S), il n’utilise pas de thread de retardateur, il fournit une file d’attente de retardateur unique et fournit un thread persistant dédié. Il fournit également des groupes de nettoyage, des performances plus élevées, plusieurs pools par processus planifiés indépendamment et une nouvelle API de pool de threads.

L’architecture du pool de threads comprend ce qui suit :

  • Des threads de travail qui exécutent les fonctions de rappel
  • Des threads de waiter qui attendent plusieurs handles d’attente
  • Une file d’attente de travail
  • Un pool de threads par défaut pour chaque processus
  • Une fabrique de travail qui gère les threads de travail

Meilleures pratiques

La nouvelle API de pool de threads offre plus de flexibilité et de contrôle que l’API de pool de threads d’origine. Toutefois, il existe des différences subtiles, mais importantes. Dans l’API d’origine, la réinitialisation d’attente était automatique ; dans la nouvelle API, l’attente doit être réinitialisée explicitement chaque fois. L’API d’origine gérait automatiquement l’emprunt d’identité, en transférant le contexte de sécurité du processus appelant vers le thread. Avec la nouvelle API, l’application doit définir explicitement le contexte de sécurité.

Voici les meilleures pratiques à suivre lors de l’utilisation d’un pool de threads :

  • Les threads d’un processus partagent le pool de threads. Un thread de travail unique peut exécuter plusieurs fonctions de rappel, une à la fois. Ces threads de travail sont gérés par le pool de threads. Par conséquent, ne terminez pas un thread du pool de threads en appelant TerminateThread sur le thread ou en appelant ExitThread à partir d’une fonction de rappel.

  • Une requête d’E/S peut s’exécuter sur n’importe quel thread du pool de threads. L’annulation d’E/S sur un thread de pool de threads demande une synchronisation, car la fonction d’annulation peut s’exécuter sur un thread différent de celui qui gère la demande d’E/S, ce qui peut entraîner l’annulation d’une opération inconnue. Pour éviter cela, fournissez toujours la structure OVERLAPPED avec laquelle une requête d’E/S a été lancée lors de l’appel de CancelIoEx pour les E/S asynchrones. Vous pouvez aussi utiliser votre propre synchronisation pour vous assurer qu’aucune autre E/S ne peut être démarrée sur le thread cible avant d’appeler la fonction CancelSynchronousIo ou CancelIoEx.

  • Nettoyez toutes les ressources créées dans la fonction de rappel avant de revenir de la fonction. Cela inclut les TLS, les contextes de sécurité, la priorité de thread et l’inscription COM. Les fonctions de rappel doivent également restaurer l’état du thread avant de retourner.

  • Conserver les handles d’attente et leurs objets associés actifs jusqu’à ce que le pool de threads ait signalé qu’il en a terminé avec le handle.

  • Marquer tous les threads en attente d’opérations longues (telles que les vidages d’E/S ou le nettoyage des ressources) afin que le pool de threads puisse allouer de nouveaux threads au lieu d’attendre celui-ci.

  • Avant de décharger une DLL qui utilise le pool de threads, annulez tous les éléments de travail, les E/S, les opérations d’attente et les retardateurs, puis attendez que les rappels d’exécution se terminent.

  • Évitez les blocages en éliminant les dépendances entre les éléments de travail et entre les rappels, en veillant à ce qu’un rappel n’attende pas qu’il se termine lui-même et en préservant la priorité des threads.

  • Ne pas mettre trop d’éléments en file d’attente trop rapidement dans un processus avec d’autres composants à l’aide du pool de threads par défaut. Il existe un pool de threads par défaut par processus, y compris Svchost.exe. Par défaut, chaque pool de threads compte un maximum de 500 threads de travail. Le pool de threads tente de créer davantage de threads de travail lorsque le nombre de ces derniers dans l’état prêt/en cours d’exécution doit être inférieur au nombre de processeurs.

  • Évitez le modèle de cloisonnement à thread unique COM, car il n’est pas compatible avec le pool de threads. Le modèle STA crée un état de thread qui peut affecter l’élément de travail suivant pour le thread. Le modèle STA est généralement de longue durée et a une affinité de thread, qui est l’opposé du pool de threads.

  • Créez un pool de threads pour contrôler la priorité et l’isolation des threads, créer des caractéristiques personnalisées et améliorer éventuellement la réactivité. Toutefois, d’autres pools de threads nécessitent davantage de ressources système (threads, mémoire du noyau). Un trop grand nombre de pools augmente le risque de contention de l’UC.

  • Dans la mesure du possible, utilisez un objet pouvant être attendu au lieu d’un mécanisme basé sur le système APC pour signaler un thread du pool de threads. Les systèmes APC ne fonctionnent pas aussi bien avec les threads de pool de threads que d’autres mécanismes de signalisation, car le système contrôle la durée de vie des threads de pool de threads. Il est donc possible qu’un thread soit arrêté avant que la notification ne soit délivrée.

  • Utilisez l’extension de débogueur du pool de threads, !tp. Cette commande est utilisée comme suit :

    • indicateurs d’adresse du pool
    • indicateurs d’adresse obj
    • indicateurs d’adresse tqueue
    • adresse de waiter
    • adresse du travail

    Pour le pool, le waiter et le travail, si l’adresse est égale à zéro, la commande vide tous les objets. Pour le waiter et le travail, omettre l’adresse vide le thread actuel. Les indicateurs suivants sont définis : 0x1 (sortie à ligne unique), 0x2 (membres de vidage) et 0x4 (file d’attente de travail du pool de vidages).

API du pool de threads

Utilisation des fonctions du pool de threads