Ports de saisie semi-automatique d’E/S

Les ports d’achèvement des E/S fournissent un modèle de threading efficace pour le traitement de plusieurs demandes 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 de file d’attente associé pour les threads dont le seul but est de traiter ces demandes. Les processus qui gèrent de nombreuses demandes d’E/S asynchrones simultanées peuvent le faire plus rapidement et plus efficacement en utilisant des ports d’achèvement d’E/S conjointement avec un pool de threads pré-alloués qu’en créant des threads au moment où ils reçoivent une demande d’E/S.

Fonctionnement des ports d’achèvement d’E/S

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

Notes

Le terme handle de fichier utilisé ici fait référence à une abstraction système représentant un point de terminaison d’E/S qui se chevauche, et pas seulement un fichier sur 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 prenant en charge les E/S qui se chevauchent 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 status 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 manière synchrone avec une erreur. Un thread (créé par le thread main ou le thread main 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 des E/S, plutôt que d’attendre directement la fin de l’E/S asynchrone. Les threads qui bloquent leur exécution sur un port d’achèvement d’E/S sont libérés dans l’ordre du dernier entré-premier sorti (LIFO), et le paquet d’achèvement suivant est extrait de la file d’attente FIFO du port d’achèvement d’E/S pour ce thread. Cela signifie que, lorsqu’un paquet d’achèvement est libéré dans 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 la plus ancienne saisie semi-automatique.

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’une des trois choses se produisent : Le thread se ferme, spécifie un port d’achèvement d’E/S différent ou ferme le port d’achèvement des E/S. En d’autres termes, un seul thread peut être associé à, au maximum, 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 à nouveau GetQueuedCompletionStatus , auquel 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 les paquets d’achèvement dans la file d’attente d’un port d’achèvement d’E/S. Ce faisant, le port d’achèvement peut être utilisé pour recevoir des communications d’autres threads du processus, en plus de recevoir des paquets d’achèvement d’E/S à partir 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 des E/S sans démarrer une opération d’E/S asynchrone. Cela est utile pour notifier les threads de travail des événements externes, par exemple.

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

Notes

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

 

Threads et accès concurrentiel

La propriété la plus importante d’un port d’achèvement d’E/S à prendre en compte avec soin est la valeur d’accès concurrentiel. La valeur d’accès concurrentiel d’un port d’achèvement est spécifiée lorsqu’il est créé avec CreateIoCompletionPort via 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 des threads suivants associés à ce port d’achèvement jusqu’à ce que le nombre de threads exécutables passe en dessous de la valeur d’accès concurrentiel.

Le scénario le plus efficace se produit lorsque des paquets d’achèvement sont en attente dans la file d’attente, mais aucune attente ne peut être satisfaite, car le port a atteint sa limite d’accès concurrentiel. Réfléchissez à ce qui se passe avec une valeur d’accès concurrentiel d’un et de 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 d’achèvement suivant mis en file d’attente. Aucun commutateur de contexte de thread ne se produit, car le thread en cours d’exécution récupère continuellement les paquets d’achèvement et les autres threads ne peuvent pas s’exécuter.

Notes

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

 

La meilleure valeur maximale globale à choisir pour la valeur d’accès concurrentiel est le nombre de processeurs sur l’ordinateur. Si votre transaction a besoin d’un calcul long, une valeur d’accès concurrentiel plus grande permettra l’exécution d’un plus grand nombre de threads. Chaque paquet d’achèvement peut prendre plus de temps, mais d’autres paquets d’achèvement seront traités en même temps. Vous pouvez tester la valeur d’accès concurrentiel conjointement avec des 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 à l’état d’attente recommence à s’exécuter, il peut y avoir une brève période où le nombre de threads actifs dépasse la valeur d’accès concurrentiel. Toutefois, le système réduit rapidement ce nombre en n’autorisant aucun nouveau thread actif tant que le nombre de threads actifs n’est pas inférieur à la valeur d’accès concurrentiel. C’est l’une des raisons pour lesquelles votre application crée plus de threads dans son pool de threads que la valeur d’accès concurrentiel. La gestion du pool de threads dépasse le cadre de cette rubrique, mais une bonne règle est d’avoir au moins deux fois plus de threads dans le pool de threads qu’il existe de 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 démarrer des opérations d’E/S qui se terminent à l’aide de ports d’achèvement d’E/S. Vous devez passer à la fonction un instance de la structure CHEVAUCHEMENT ET un handle 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

GetQueuedCompletionStatus

GetQueuedCompletionStatusEx

PostQueuedCompletionStatus