Planification en mode utilisateur

Avertissement

À compter de Windows 11, la planification en mode utilisateur n’est pas prise en charge. Tous les appels échouent avec l’erreur ERROR_NOT_SUPPORTED.

La planification en mode utilisateur (UMS) est un mécanisme léger que les applications peuvent utiliser pour planifier leurs propres threads. Une application peut basculer entre les threads UMS en mode utilisateur sans impliquer le planificateur système et reprendre le contrôle du processeur si un thread UMS bloque dans le noyau. Les threads UMS diffèrent des fibres, car chaque thread UMS a son propre contexte de thread au lieu de partager le contexte de thread d’un seul thread. La possibilité de basculer entre les threads en mode utilisateur rend UMS plus efficace que les pools de threads pour gérer un grand nombre d’éléments de travail de courte durée qui nécessitent peu d’appels système.

UMS est recommandé pour les applications ayant des exigences hautes performances qui doivent exécuter efficacement de nombreux threads simultanément sur des systèmes multiprocesseurs ou multicœurs. Pour tirer parti de UMS, une application doit implémenter un composant planificateur qui gère les threads UMS de l’application et détermine quand elles doivent s’exécuter. Les développeurs doivent déterminer si leurs exigences en matière de performances d’application justifient le travail impliqué dans le développement d’un tel composant. Les applications avec des exigences de performances modérées peuvent être mieux prises en charge en permettant au planificateur système de planifier leurs threads.

UMS est disponible pour les applications 64 bits s’exécutant sur les versions AMD64 et Itanium de Windows 7 et Windows Server 2008 R2 à Windows 10 version 21H2 et Windows Server 2022. Cette fonctionnalité n’est pas disponible sur arm64, versions 32 bits de Windows ou sur Windows 11.

Pour plus d’informations, consultez les sections suivantes :

Planificateur UMS

Le planificateur UMS d’une application est chargé de créer, de gérer et de supprimer des threads UMS et de déterminer le thread UMS à exécuter. Le planificateur d’une application effectue les tâches suivantes :

  • Crée un thread du planificateur UMS pour chaque processeur sur lequel l’application exécutera des threads de travail UMS.
  • Crée des threads de travail UMS pour effectuer le travail de l’application.
  • Gère sa propre file d’attente de threads de travail prête à l’exécution et sélectionne les threads à exécuter en fonction des stratégies de planification de l’application.
  • Crée et surveille une ou plusieurs listes d’achèvement où les threads système sont mis en file d’attente une fois qu’ils ont terminé le traitement dans le noyau. Ceux-ci incluent les threads de travail et les threads nouvellement créés précédemment bloqués sur un appel système qui deviennent débloqués.
  • Fournit une fonction de point d’entrée du planificateur pour gérer les notifications du système. Le système appelle la fonction de point d’entrée lorsqu’un thread du planificateur est créé, un thread de travail bloque sur un appel système ou un thread de travail génère explicitement le contrôle.
  • Effectue des tâches de nettoyage pour les threads de travail qui ont terminé l’exécution.
  • Effectue un arrêt ordonné du planificateur lorsqu’il est demandé par l’application.

Thread planificateur UMS

Un thread planificateur UMS est un thread ordinaire qui s’est converti en UMS en appelant la fonction EnterUmsSchedulingMode . Le planificateur système détermine quand le thread du planificateur UMS s’exécute en fonction de sa priorité par rapport aux autres threads prêts. Le processeur sur lequel le thread du planificateur s’exécute est influencé par l’affinité du thread, identique à celle des threads non-UMS.

L’appelant d’EnterUmsSchedulingMode spécifie une liste de saisie semi-automatique et une fonction de point d’entrée UmsSchedulerProc à associer au thread du planificateur UMS. Le système appelle la fonction de point d’entrée spécifiée quand il est terminé de convertir le thread appelant en UMS. La fonction de point d’entrée du planificateur est chargée de déterminer l’action suivante appropriée pour le thread spécifié. Pour plus d’informations, consultez la fonction de point d’entrée du planificateur UMS plus loin dans cette rubrique.

Une application peut créer un thread planificateur UMS pour chaque processeur qui sera utilisé pour exécuter des threads UMS. L’application peut également définir l’affinité de chaque thread du planificateur UMS pour un processeur logique spécifique, ce qui a tendance à exclure les threads non liés de l’exécution sur ce processeur, en le réservant efficacement pour ce thread du planificateur. N’oubliez pas que la définition de l’affinité de thread de cette façon peut affecter les performances globales du système en affaissant d’autres processus qui peuvent s’exécuter sur le système. Pour plus d’informations sur l’affinité de thread, consultez Plusieurs processeurs.

Threads de travail UMS, contextes de thread et listes de saisie semi-automatique

Un thread de travail UMS est créé en appelant CreateRemoteThreadEx avec l’attribut PROC_THREAD_ATTRIBUTE_UMS_THREAD et en spécifiant un contexte de thread UMS et une liste de saisie semi-automatique.

Un contexte de thread UMS représente l’état du thread UMS d’un thread de travail et est utilisé pour identifier le thread de travail dans les appels de fonction UMS. Il est créé en appelant CreateUmsThreadContext.

Une liste de saisie semi-automatique est créée en appelant la fonction CreateUmsCompletionList . Une liste de saisie semi-automatique reçoit des threads de travail UMS qui ont terminé l’exécution dans le noyau et sont prêts à s’exécuter en mode utilisateur. Seul le système peut mettre en file d’attente les threads de travail vers une liste de saisie semi-automatique. Les nouveaux threads de travail UMS sont automatiquement mis en file d’attente vers la liste de saisie semi-automatique spécifiée lors de la création des threads. Les threads de travail précédemment bloqués sont également mis en file d’attente vers la liste de saisie semi-automatique lorsqu’ils ne sont plus bloqués.

Chaque thread du planificateur UMS est associé à une seule liste de saisie semi-automatique. Toutefois, la même liste de saisie semi-automatique peut être associée à n’importe quel nombre de threads du planificateur UMS, et un thread planificateur peut récupérer des contextes UMS à partir de n’importe quelle liste de saisie semi-automatique pour laquelle il a un pointeur.

Chaque liste de saisie semi-automatique a un événement associé signalé par le système lorsqu’il met en file d’attente un ou plusieurs threads de travail vers une liste vide. La fonction GetUmsCompletionListEvent récupère un handle à l’événement pour une liste de saisie semi-automatique spécifiée. Une application peut attendre plusieurs événements de liste de saisie semi-automatique, ainsi que d’autres événements logiques pour l’application.

Fonction de point d’entrée du planificateur UMS

La fonction de point d’entrée du planificateur d’une application est implémentée en tant que fonction UmsSchedulerProc . Le système appelle la fonction de point d’entrée du planificateur de l’application aux heures suivantes :

  • Lorsqu’un thread non-UMS est converti en thread de planificateur UMS en appelant EnterUmsSchedulingMode.
  • Lorsqu’un thread de travail UMS appelle UmsThreadYield.
  • Lorsqu’un thread de travail UMS bloque sur un service système tel qu’un appel système ou une erreur de page.

Le paramètre Reason de la fonction UmsSchedulerProc spécifie la raison pour laquelle la fonction de point d’entrée a été appelée. Si la fonction de point d’entrée a été appelée car un nouveau thread du planificateur UMS a été créé, le paramètre SchedulerParam contient les données spécifiées par l’appelant d’EnterUmsSchedulingMode. Si la fonction de point d’entrée a été appelée parce qu’un thread de travail UMS a généré, le paramètre SchedulerParam contient les données spécifiées par l’appelant de UmsThreadYield. Si la fonction de point d’entrée a été appelée car un thread de travail UMS bloqué dans le noyau, le paramètre SchedulerParam est NULL.

La fonction de point d’entrée du planificateur est chargée de déterminer l’action suivante appropriée pour le thread spécifié. Par exemple, si un thread de travail est bloqué, la fonction de point d’entrée du planificateur peut exécuter le prochain thread de travail UMS prêt à l’emploi.

Lorsque la fonction de point d’entrée du planificateur est appelée, le planificateur de l’application doit tenter de récupérer tous les éléments de sa liste d’achèvement associée en appelant la fonction DequeueUmsCompletionListItems . Cette fonction récupère une liste de contextes de thread UMS qui ont terminé le traitement dans le noyau et sont prêts à s’exécuter en mode utilisateur. Le planificateur de l’application ne doit pas exécuter les threads UMS directement à partir de cette liste, car cela peut entraîner un comportement imprévisible dans l’application. Au lieu de cela, le planificateur doit récupérer tous les contextes de thread UMS en appelant la fonction GetNextUmsListItem une fois pour chaque contexte, insérer les contextes de thread UMS dans la file d’attente de threads prête du planificateur, puis exécuter les threads UMS à partir de la file d’attente de threads prêtes.

Si le planificateur n’a pas besoin d’attendre plusieurs événements, il doit appeler DequeueUmsCompletionListItems avec un paramètre de délai d’attente différent de zéro afin que la fonction attend l’événement de liste d’achèvement avant de retourner. Si le planificateur doit attendre plusieurs événements de liste d’achèvement, il doit appeler DequeueUmsCompletionListItems avec un paramètre de délai d’expiration égal à zéro afin que la fonction retourne immédiatement, même si la liste d’achèvement est vide. Dans ce cas, le planificateur peut attendre explicitement sur les événements de liste d’achèvement, par exemple à l’aide de WaitForMultipleObjects.

Exécution de thread UMS

Un thread de travail UMS nouvellement créé est mis en file d’attente vers la liste d’achèvement spécifiée et ne commence pas à s’exécuter tant que le planificateur UMS de l’application ne le sélectionne pas pour s’exécuter. Cela diffère des threads non-UMS, que le planificateur système planifie automatiquement l’exécution, sauf si l’appelant crée explicitement le thread suspendu.

Le planificateur exécute un thread de travail en appelant ExecuteUmsThread avec le contexte UMS du thread de travail. Un thread de travail UMS s’exécute jusqu’à ce qu’il soit généré en appelant la fonction UmsThreadYield , les blocs ou les termine.

Meilleures pratiques UMS

Les applications qui implémentent UMS doivent suivre les meilleures pratiques suivantes :

  • Les structures sous-jacentes pour les contextes de thread UMS sont gérées par le système et ne doivent pas être modifiées directement. Utilisez plutôt QueryUmsThreadInformation et SetUmsThreadInformation pour récupérer et définir des informations sur un thread de travail UMS.
  • Pour éviter les blocages, le thread du planificateur UMS ne doit pas partager de verrous avec des threads de travail UMS. Cela inclut les verrous créés par l’application et les verrous système qui sont acquis indirectement par des opérations telles que l’allocation à partir du tas ou le chargement de DLL. Par exemple, supposons que le planificateur exécute un thread de travail UMS qui charge une DLL. Le thread de travail acquiert le verrou et les blocs du chargeur. Le système appelle la fonction de point d’entrée du planificateur, qui charge ensuite une DLL. Cela provoque un interblocage, car le verrou du chargeur est déjà conservé et ne peut pas être libéré tant que le premier thread ne se débloque pas. Pour éviter ce problème, le travail délégué qui peut partager des verrous avec des threads de travail UMS vers un thread de travail UMS dédié ou un thread non-UMS.
  • UMS est plus efficace lorsque la plupart du traitement est effectué en mode utilisateur. Dans la mesure du possible, évitez d’effectuer des appels système dans des threads de travail UMS.
  • Les threads de travail UMS ne doivent pas supposer que le planificateur système est utilisé. Cette hypothèse peut avoir des effets subtils; Par exemple, si un thread dans le code inconnu définit une priorité ou une affinité de thread, le planificateur UMS peut toujours le remplacer. Le code qui suppose que le planificateur système est utilisé peut ne pas se comporter comme prévu et peut s’interrompre lorsqu’il est appelé par un thread UMS.
  • Le système peut avoir besoin de verrouiller le contexte de thread d’un thread de travail UMS. Par exemple, un appel de procédure asynchrone en mode noyau (APC) peut modifier le contexte du thread UMS, de sorte que le contexte de thread doit être verrouillé. Si le planificateur tente d’exécuter le contexte de thread UMS pendant qu’il est verrouillé, l’appel échoue. Ce comportement est par conception et le planificateur doit être conçu pour réessayer l’accès au contexte de thread UMS.