Votre compréhension de l'asynchronisme est un peu fausse. Il permet au thread d'être renvoyé dans le pool s'il est dans un état d'attente . Ce dernier point est important. Le thread est toujours en train d'exécuter le code, c'est juste qu'un processus externe est en cours, qui ne nécessite pas ce thread pour le moment, c'est-à-dire une communication réseau, une entrée/sortie de fichier, etc. Une fois que ce processus externe est terminé, le système d'exploitation est responsable de la restauration du contrôle du processus d'origine, qui nécessite alors qu'un nouveau thread soit demandé au pool pour continuer.
De plus, ce n'est pas parce qu'une partie de votre code est asynchrone que tout se passe de manière asynchrone : à savoir, uniquement les autres codes qui sont asynchrones et qui peuvent réellement l'être. Comme je l'ai dit plus haut, le thread n'est libéré que s'il est dans un état d'attente ; exécuter quelque chose de synchrone signifierait qu'il n'est pas en attente, et donc qu'il ne sera pas libéré. De plus, certaines choses ne seront jamais asynchrones, même si vous essayez de les exécuter de manière asynchrone. Tout ce qui est lié au CPU nécessite l'exécution du thread, donc le thread n'entrera jamais dans un état d'attente et ne sera jamais libéré.
UPDATE
L'hypothèse semble être qu'il y a un autre thread qui gère réellement le travail asynchrone, mais ce n'est pas ce qui se passe. Comme je l'ai dit dans les commentaires, le travail au niveau du système d'exploitation est extrêmement technique, mais Voici la meilleure explication simplifiée que j'ai trouvée. . La partie pertinente de l'article est ci-dessous pour la postérité :
Qu'en est-il du thread qui effectue le travail asynchrone ?
On me pose cette question tout le temps. L'implication est qu'il doit y avoir un thread quelque part qui bloque sur l'appel I/O à la ressource externe. Donc, le code asynchrone libère le thread de la requête, mais seulement au détriment d'un autre thread ailleurs dans le système, n'est-ce pas ? Non, pas du tout.
Pour comprendre pourquoi les requêtes asynchrones ont une échelle, je vais tracer un exemple (simplifié) d'un appel d'E/S asynchrone. Disons qu'une requête doit écrire dans un fichier. Le thread de la requête appelle la méthode d'écriture asynchrone. WriteAsync est implémenté par la Base Class Library (BCL), et utilise des ports de complétion pour ses E/S asynchrones. Ainsi, l'appel WriteAsync est transmis au système d'exploitation comme une écriture asynchrone de fichier. Le système d'exploitation communique ensuite avec la pile de pilotes, en transmettant les données à écrire dans un paquet de demandes d'E/S (IRP).
C'est là que les choses deviennent intéressantes : Si un pilote de périphérique ne peut pas traiter une IRP immédiatement, il doit la traiter de manière asynchrone. Ainsi, le pilote indique au disque de commencer à écrire et renvoie une réponse "en attente" au système d'exploitation. Le système d'exploitation transmet cette réponse "en attente" au BCL, et le BCL renvoie une tâche incomplète au code de traitement des demandes. Le code de traitement des demandes attend la tâche, qui renvoie une tâche incomplète de cette méthode et ainsi de suite. Enfin, le code de traitement des demandes finit par renvoyer une tâche incomplète à ASP.NET, et le thread de la demande est libéré pour retourner dans le pool de threads.
Considérons maintenant l'état actuel du système. Plusieurs structures d'E/S ont été allouées (par exemple, les instances Task et l'IRP), et elles sont toutes dans un état d'attente/incomplet. Cependant, aucun thread n'est bloqué en attendant que l'opération d'écriture soit terminée. Ni ASP.NET, ni la BCL, ni le système d'exploitation, ni le pilote de périphérique n'ont de thread dédié au travail asynchrone.
Lorsque le disque a fini d'écrire les données, il en informe son pilote par une interruption. Le pilote informe le système d'exploitation que l'IRP est terminée, et le système d'exploitation notifie le BCL via le port d'achèvement. Un thread du pool de threads répond à cette notification en complétant la tâche qui a été renvoyée par WriteAsync ; ceci reprend à son tour le code de la requête asynchrone. Quelques threads ont été "empruntés" pour de très courtes durées pendant cette phase de notification d'achèvement, mais aucun thread n'a été réellement bloqué pendant que l'écriture était en cours.
Cet exemple est radicalement simplifié, mais il met en évidence le point essentiel : aucun thread n'est nécessaire pour un véritable travail asynchrone. Aucun temps CPU n'est nécessaire pour envoyer les octets. Il y a aussi une autre leçon à tirer. Pensez au monde des pilotes de périphériques, à la façon dont un pilote de périphérique doit soit traiter une IRP immédiatement, soit de manière asynchrone. La gestion synchrone n'est pas une option. Au niveau du pilote de périphérique, toutes les E/S non triviales sont asynchrones. De nombreux développeurs ont un modèle mental qui traite l'"API naturelle" pour les opérations d'E/S comme synchrone, avec l'API asynchrone comme une couche construite sur l'API naturelle, synchrone. Cependant, c'est complètement à rebours : en fait, l'API naturelle est asynchrone, et ce sont les API synchrones qui sont mises en œuvre à l'aide d'E/S asynchrones !