Ports de finalisation d’E/S

Les ports de terminaison d'E/S fournissent un modèle de threading efficace pour traiter plusieurs requêtes d'E/S asynchrones sur un système multiprocesseur. Lorsqu’un processus crée un port d’achèvement d’E/S, le système crée un objet file d’attente associé pour les threads dont le seul objectif est de traiter ces requêtes. Les processus qui gèrent de nombreuses requêtes d’E/S asynchrones simultanées peuvent le faire plus rapidement et efficacement à l’aide de ports d’achèvement d’E/S conjointement avec un pool de threads pré-alloué que en créant des threads au moment où ils reçoivent une requête d’E/S.

Quand utiliser des ports d’achèvement d’E/S

Scénario Approche recommandée
Serveur hautes performances gérant des centaines/milliers de connexions simultanées Ports de fin d’E/S — conçus spécifiquement à cet effet. Le noyau gère la planification des threads pour qu’elle corresponde à la concurrence du processeur.
Concurrence modérée (dizaines d’opérations asynchrones) E/S du pool de threads (CreateThreadpoolIo) : API plus simple, gère le IOCP en interne. Préférez le nouveau code qui n’a pas besoin de contrôle de thread manuel.
Opérations de fichiers asynchrones simples en C++ moderne Coroutines C++20 avec un répartiteur IOCP personnalisé, ou .NET FileStream avec async/await.
E/S mono-thread ou de faible volume E/S synchrones ou E/S superposées simples avec signalisation d’événement. IOCP ajoute une complexité inutile pour les scénarios à flux unique.

Remarque

API de pool de threads et IOCP brute : l’API de pool de threads Windows (CreateThreadpoolIo, StartThreadpoolIo) utilise IOCP en interne, mais gère automatiquement la gestion du cycle de vie des threads. Pour les nouvelles applications serveur, envisagez d’abord l’API du pool de threads : elle fournit la même scalabilité avec moins de code réutilisable. Utilisez le IOCP brut lorsque vous avez besoin d’un contrôle explicite sur la valeur de concurrence du port d’achèvement ou la gestion personnalisée des threads.

Fonctionnement des ports de terminaison d'E/S

La fonction CreateIoCompletionPort crée un port d’achèvement d’E/S et associe un ou plusieurs handles de fichier à ce port. Lorsqu’une opération d’E/S asynchrone sur l’un de ces descripteurs de fichier se termine, un paquet d’achèvement d’E/S est placé dans la file d’attente selon l’ordre premier entré, premier sorti (FIFO) sur le port d’achèvement d’E/S associé. Une utilisation puissante pour ce mécanisme consiste à combiner le point de synchronisation pour plusieurs handles de fichiers en un seul objet, bien qu’il existe également d’autres applications utiles. Notez que, bien que les paquets soient mis en file d’attente dans l’ordre FIFO, ils peuvent être retirés de la file d’attente dans un ordre différent.

Remarque

Le terme handle de fichier comme utilisé ici fait référence à une abstraction système représentant un point de terminaison d’E/S superposé, pas seulement un fichier sur le disque. Par exemple, il peut s’agir d’un point de terminaison réseau, d’un socket TCP, d’un canal nommé ou d’un emplacement de messagerie. Tout objet système qui prend en charge les E/S superposées peut être utilisé. Pour obtenir la liste des fonctions d’E/S associées, consultez la fin de cette rubrique.

Lorsqu’un handle de fichier est associé à un port d’achèvement, le bloc d’état transmis n’est pas mis à jour tant que le paquet n’est pas supprimé du port d’achèvement. La seule exception est si l’opération d’origine retourne de façon synchrone avec une erreur. Un thread (créé par le thread principal ou le thread principal lui-même) utilise la fonction GetQueuedCompletionStatus pour attendre qu’un paquet d’achèvement soit mis en file d’attente vers le port d’achèvement d’E/S, plutôt que d’attendre directement la fin de l’E/S asynchrone. Les threads qui se bloquent sur un port d’achèvement d’E/S sont débloqués dans l’ordre dernier entré, premier sorti (LIFO), et, pour ce thread, le paquet d’achèvement suivant est extrait de la file d’attente FIFO du port d’achèvement d’E/S. Cela signifie que, lorsqu’un paquet d’achèvement est libéré sur un thread, le système libère le dernier thread (le plus récent) associé à ce port, en lui transmettant les informations d’achèvement pour l’achèvement des E/S les plus anciennes.

Bien qu’un nombre quelconque de threads puisse appeler GetQueuedCompletionStatus pour un port d’achèvement d’E/S spécifié, lorsqu’un thread spécifié appelle GetQueuedCompletionStatus la première fois, il devient associé au port d’achèvement d’E/S spécifié jusqu’à ce que l’un des trois éléments se produise : le thread quitte, spécifie un port d’achèvement d’E/S différent ou ferme le port d’achèvement d’E/S spécifié. En d’autres termes, un seul thread peut être associé à, au plus, un port d’achèvement d’E/S.

Lorsqu’un paquet d’achèvement est mis en file d’attente vers un port d’achèvement d’E/S, le système vérifie d’abord le nombre de threads associés à ce port en cours d’exécution. Si le nombre de threads en cours d’exécution est inférieur à la valeur d’accès concurrentiel (décrite dans la section suivante), l’un des threads en attente (le plus récent) est autorisé à traiter le paquet d’achèvement. Lorsqu’un thread en cours d’exécution termine son traitement, il appelle généralement GetQueuedCompletionStatus à nouveau, à quel moment il retourne avec le paquet d’achèvement suivant ou attend si la file d’attente est vide.

Les threads peuvent utiliser la fonction PostQueuedCompletionStatus pour placer des paquets d’achèvement dans la file d’attente du port d’achèvement d’E/S. Ainsi, le port de fin d’E/S peut être utilisé pour recevoir des communications provenant d’autres threads du processus, en plus des paquets de fin d’E/S du système d’E/S. La fonction PostQueuedCompletionStatus permet à une application de mettre en file d’attente ses propres paquets d’achèvement à usage spécial vers le port d’achèvement d’E/S sans démarrer une opération d’E/S asynchrone. Cela est utile pour avertir les threads de travail des événements externes, par exemple.

Le handle du port d’achèvement des E/S et chaque handle de fichier associé à ce port d’achèvement des E/S particulier sont appelés des références au port d’achèvement des E/S. Le port de terminaison d'E/S est libéré lorsqu'il n'existe plus aucune référence à celui-ci. Par conséquent, tous ces handles doivent être correctement fermés afin de libérer le port de terminaison d'E/S et les ressources système qui lui sont associées. Une fois ces conditions remplies, une application doit fermer le handle du port de terminaison d'E/S en appelant la fonction CloseHandle.

Remarque

Un port d’achèvement d’E/S est associé au processus qui l’a créé et n’est pas partagé entre les processus. Toutefois, un handle unique peut être partagé entre les threads du même processus.

Threads et concurrence

La propriété la plus importante d'un port de terminaison d'E/S à prendre soigneusement en compte est la valeur de concurrence. La valeur de concurrence d’un port d’achèvement est spécifiée lors de sa création par CreateIoCompletionPort avec le paramètre NumberOfConcurrentThreads. Cette valeur limite le nombre de threads exécutables associés au port d’achèvement. Lorsque le nombre total de threads exécutables associés au port d’achèvement atteint la valeur d’accès concurrentiel, le système bloque l’exécution de tous les threads suivants associés à ce port d’achèvement jusqu’à ce que le nombre de threads exécutables tombe sous la valeur d’accès concurrentiel.

Le scénario le plus efficace se produit lorsque des paquets de terminaison sont en attente dans la file d'attente, mais qu'aucune attente ne peut être satisfaite, car le port a atteint sa limite de concurrence. Examinez ce qui se produit avec une valeur de concurrence de un et plusieurs threads en attente dans l'appel de fonction GetQueuedCompletionStatus. Dans ce cas, si la file d’attente a toujours des paquets d’achèvement en attente, lorsque le thread en cours d’exécution appelle GetQueuedCompletionStatus, il ne bloque pas l’exécution car, comme mentionné précédemment, la file d’attente de threads est LIFO. Au lieu de cela, ce thread récupère immédiatement le paquet de terminaison suivant dans la file d'attente. Aucun changement de contexte entre threads ne se produira, car le thread en cours d’exécution traite en continu les paquets d’achèvement et les autres threads ne peuvent pas s’exécuter.

Remarque

Dans l’exemple précédent, les threads supplémentaires semblent inutiles et ne s’exécutent jamais, mais suppose que le thread en cours d’exécution n’est jamais mis dans un état d’attente par un autre mécanisme, se termine ou ferme son port d’achèvement d’E/S associé. Considérez toutes ces ramifications d’exécution de thread lors de la conception de l’application.

La meilleure valeur maximale globale à choisir pour la valeur de concurrence est le nombre de processeurs sur l’ordinateur. Si votre transaction a nécessité un calcul long, une valeur de concurrence plus élevée permettra à davantage de threads de s’exécuter. Chaque paquet d’achèvement peut prendre plus de temps, mais plus de paquets d’achèvement seront traités en même temps. Vous pouvez expérimenter la valeur de concurrence conjointement avec les outils de profilage pour obtenir le meilleur effet pour votre application.

Le système permet également à un thread en attente dans GetQueuedCompletionStatus de traiter un paquet d’achèvement si un autre thread en cours d’exécution associé au même port d’achèvement d’E/S entre dans un état d’attente pour d’autres raisons, par exemple la fonction SuspendThread . Lorsque le thread en état d’attente reprend son exécution, il peut y avoir une brève période pendant laquelle le nombre de threads actifs dépasse le niveau de concurrence. Toutefois, le système réduit rapidement ce nombre en n’autorisant pas de nouveaux threads actifs tant que le nombre de threads actifs n’est pas inférieur à la valeur de concurrence. C'est l'une des raisons pour lesquelles votre application doit créer dans son pool de threads un nombre de threads supérieur à la valeur de concurrence. La gestion des pools de threads dépasse l’étendue de cette rubrique, mais une bonne règle de pouce consiste à avoir au moins deux fois plus de threads dans le pool de threads qu’il existe des processeurs sur le système. Pour plus d’informations sur le regroupement de threads, consultez pools de threads.

Fonctions d’E/S prises en charge

Les fonctions suivantes peuvent être utilisées pour lancer des opérations d’E/S qui s’achèvent via des ports de fin d’E/S. Vous devez passer à la fonction une instance de la structure OVERLAPPED et un descripteur de fichier précédemment associé à un port d’achèvement d’E/S (par un appel à CreateIoCompletionPort) pour activer le mécanisme de port d’achèvement d’E/S :

à propos des processus et des threads

BindIoCompletionCallback

CreateIoCompletionPort