Je ne comprends pas exactement ce qui se passe dans les coulisses lorsque j'ai une action asynchrone sur un contrôleur MVC, en particulier lorsqu'il s'agit d'opérations d'E/S. Disons que j'ai une action de téléchargement:
public async Task<ActionResult> Upload (HttpPostedFileBase file) {
....
await ReadFile(file);
...
}
D'après ce que je sais, ce sont les étapes de base qui se produisent:
Un nouveau thread est extrait de threadpool et affecté à la gestion de la demande entrante.
Lorsque l'attente est frappée, si l'appel est une opération d'E/S, le thread d'origine revient dans le pool et le contrôle est transféré vers un soi-disant IOCP (port de fin de sortie d'entrée). Ce que je ne comprends pas, c'est pourquoi la demande est toujours en vie et attend une réponse car, à la fin, le client appelant attendra la fin de notre demande.
Ma question est: qui/quand/comment cette attente pour un blocage complet se produit-elle?
Remarque: j'ai vu le billet de blog Il n'y a pas de fil, et cela a du sens pour les applications GUI, mais pour ce côté serveur scénario, je ne comprends pas. Vraiment.
Il y a de bonnes ressources sur le net qui décrivent cela en détail. J'ai écrit un article MSDN qui décrit cela à un niveau élevé .
Ce que je ne comprends pas, c'est pourquoi la demande est toujours en vie et attend une réponse car, à la fin, le client appelant attendra la fin de notre demande.
Il est toujours actif car le runtime ASP.NET ne l'a pas encore terminé. Compléter la demande (en envoyant la réponse) est une action explicite; ce n'est pas comme si la demande se terminait d'elle-même. Lorsque ASP.NET voit que l'action du contrôleur renvoie un Task
/Task<T>
, il ne terminera pas la demande tant que cette tâche ne sera pas terminée.
Ma question est: qui/quand/comment cette attente pour un blocage complet se produit-elle?
Rien n'attend.
Pensez-y de cette façon: ASP.NET a une collection de demandes en cours qu'il traite. Pour une demande donnée, dès qu'elle est terminée, la réponse est envoyée puis cette demande est supprimée de la collection.
La clé est que c'est une collection de requêtes, pas de threads. Chacune de ces demandes peut ou non avoir un thread qui fonctionne dessus à tout moment. Les demandes synchrones ont toujours un seul thread (le même thread). Les demandes asynchrones peuvent avoir des périodes où elles n'ont pas de threads.
Remarque: j'ai vu ce fil: http://blog.stephencleary.com/2013/11/there-is-no-thread.html et cela a du sens pour les applications GUI mais pour ce scénario côté serveur Je ne comprends pas.
L'approche sans fil des E/S fonctionne exactement de la même manière pour les applications ASP.NET que pour les applications GUI.
Finalement, l'écriture du fichier se terminera, ce qui terminera (éventuellement) la tâche renvoyée par ReadFile
. Ce travail de "fin de la tâche" se fait normalement avec un thread de pool de threads. Comme la tâche est maintenant terminée, l'action Upload
continuera de s'exécuter, ce qui fera que ce thread entrera dans le contexte de la requête (c'est-à-dire qu'il y a maintenant un thread qui exécute à nouveau cette requête). Lorsque la méthode Upload
est terminée, la tâche renvoyée par Upload
est terminée et ASP.NET écrit la réponse et supprime la demande de sa collection.
Sous le capot, le compilateur effectue un tour de main et transforme votre code async
\await
en un code basé sur Task
avec un rappel. Dans le cas le plus simple:
public async Task X()
{
A();
await B();
C();
}
Obtient changé en quelque chose comme:
public Task X()
{
A();
return B().ContinueWith(()=>{ C(); })
}
Il n'y a donc pas de magie - juste beaucoup de Task
et de rappels. Pour un code plus complexe, les transformations seront également plus complexes, mais au final le code résultant sera logiquement équivalent à ce que vous avez écrit. Si vous le souhaitez, vous pouvez prendre un de ILSpy/Reflector/JustDecompile et voir par vous-même ce qui est compilé "sous le capot".
À son tour, l'infrastructure ASP.NET MVC est suffisamment intelligente pour reconnaître si votre méthode d'action est normale ou basée sur Task
et modifier son comportement à son tour. Par conséquent, la demande ne "disparaît" pas.
Une idée fausse commune est que tout avec async
génère un autre thread. En fait, c'est surtout le contraire. Au bout de la longue chaîne du async Task
méthodes, il existe normalement une méthode qui exécute des opérations asynchrones IO (telles que la lecture à partir du disque ou la communication via le réseau), ce qui est magique par Windows lui-même. Pendant la durée de cette opération) , aucun thread n'est associé au code - il est effectivement arrêté. Une fois l'opération terminée, Windows rappelle cependant, puis un thread du pool de threads est affecté à la poursuite de l'exécution. Il y a un peu de code de structure à préserver le HttpContext
de la requête, mais c'est tout.
Le runtime ASP.NET comprend les tâches et retarde l'envoi de la réponse HTTP jusqu'à ce que la tâche soit terminée. En fait, le Task.Result
une valeur est nécessaire pour même générer une réponse.
Le runtime fait essentiellement ceci:
var t = Upload(...);
t.ContinueWith(_ => SendResponse(t));
Ainsi, lorsque votre await
est frappé à la fois votre code et le code d'exécution quitte la pile et "il n'y a pas de thread" à ce stade. Le rappel ContinueWith
fait revivre la demande et envoie la réponse.