Après avoir traité le modèle asynchrone / attente de C # pendant un certain temps maintenant, je me suis soudain rendu compte que je ne savais pas vraiment comment expliquer ce qui se passait dans le code suivant:
async void MyThread()
{
while (!_quit)
{
await GetWorkAsync();
}
}
GetWorkAsync()
est supposé renvoyer un fichier attendu Task
qui peut ou non provoquer un changement de thread lorsque la continuation est exécutée.
Je ne serais pas confus si l'attente n'était pas dans une boucle. Je m'attendrais naturellement à ce que le reste de la méthode (c'est-à-dire la poursuite) s'exécute potentiellement sur un autre thread, ce qui est bien.
Cependant, à l'intérieur d'une boucle, le concept de «le reste de la méthode» me brouille un peu.
Qu'arrive-t-il au "reste de la boucle" si le thread est activé en continuation ou s'il n'est pas activé? Sur quel thread la prochaine itération de la boucle est-elle exécutée?
Mes observations montrent (non vérifiées de façon concluante) que chaque itération commence sur le même thread (l'original) tandis que la suite s'exécute sur un autre. Est-ce vraiment possible? Si oui, s'agit-il alors d'un degré de parallélisme inattendu qui doit être pris en compte vis-à-vis de la sécurité des threads de la méthode GetWorkAsync?
MISE À JOUR: Ma question n'est pas un doublon, comme suggéré par certains. Le while (!_quit) { ... }
modèle de code n'est qu'une simplification de mon code actuel. En réalité, mon thread est une boucle longue durée qui traite sa file d'attente d'entrée d'éléments de travail à intervalles réguliers (toutes les 5 secondes par défaut). La vérification de la condition de sortie réelle n'est pas non plus une simple vérification de champ comme le suggère l'exemple de code, mais plutôt une vérification de la gestion des événements.
Réponses:
Vous pouvez le vérifier sur Try Roslyn . Votre méthode d'attente est réécrite dans
void IAsyncStateMachine.MoveNext()
la classe asynchrone générée.Ce que vous verrez est quelque chose comme ceci:
Fondamentalement, peu importe le fil sur lequel vous vous trouvez; la machine d'état peut reprendre correctement en remplaçant votre boucle par une structure if / goto équivalente.
Cela dit, les méthodes asynchrones ne s'exécutent pas nécessairement sur un thread différent. Voir l'explication d'Eric Lippert "Ce n'est pas magique" pour expliquer comment vous pouvez travailler
async/await
sur un seul thread.la source
Premièrement, Servy a écrit du code dans une réponse à une question similaire, sur laquelle cette réponse est basée:
/programming/22049339/how-to-create-a-cancellable-task-loop
La réponse de Servy comprend une
ContinueWith()
boucle similaire utilisant des constructions TPL sans utilisation explicite des mots clésasync
etawait
; donc pour répondre à votre question, réfléchissez à ce que pourrait être votre code lorsque votre boucle est déroulée à l'aideContinueWith()
Cela prend un certain temps pour vous envelopper, mais en résumé:
continuation
représente une fermeture pour "l'itération en cours"previous
représente l'Task
état contenant "l'itération précédente" (c'est-à-dire qu'il sait quand l'itération est terminée et est utilisé pour démarrer la suivante ..)GetWorkAsync()
renvoie aTask
, cela signifieContinueWith(_ => GetWorkAsync())
retournera d'Task<Task>
où l'appel àUnwrap()
obtenir la «tâche intérieure» (c'est-à-dire le résultat réel deGetWorkAsync()
).Donc:
Task.FromResult(_quit)
- son état commence parTask.Completed == true
.continuation
est exécuté pour la première fois à l' aideprevious.ContinueWith(continuation)
continuation
misesprevious
à jour de fermeture pour refléter l'état d'achèvement de_ => GetWorkAsync()
_ => GetWorkAsync()
terminé, il "continue avec"_previous.ContinueWith(continuation)
- c'est-à-dire appeler àcontinuation
nouveau le lambdaprevious
a été mis à jour avec l'état de_ => GetWorkAsync()
sorte que lecontinuation
lambda est appelé lors duGetWorkAsync()
retour.Le
continuation
lambda vérifie toujours l'état de_quit
so, s'il_quit == false
n'y a plus de continuations, etTaskCompletionSource
obtient la valeur de_quit
, et tout est terminé.Quant à votre observation concernant la poursuite de l'exécution dans un autre thread, ce n'est pas quelque chose que le mot
async
-await
clé / ferait pour vous, selon ce blog "Les tâches ne sont (toujours) pas des threads et async n'est pas parallèle" . - https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/Je dirais qu'il vaut en effet la peine d'examiner de plus près votre
GetWorkAsync()
méthode en ce qui concerne le filetage et la sécurité du fil. Si vos diagnostics révèlent qu'il s'est exécuté sur un autre thread à la suite de votre code asynchrone / attente répété, alors quelque chose dans ou lié à cette méthode doit provoquer la création d'un nouveau thread ailleurs. (Si c'est inattendu, il y a peut-être un.ConfigureAwait
endroit?)la source
SynchronizationContext
- qui est certainement important car.ContinueWith()
utilise le SynchronizationContext pour distribuer la suite; cela expliquerait en effet le comportement que vous voyez siawait
est appelé sur un thread ThreadPool ou un thread ASP.NET. Une suite pourrait certainement être envoyée à un fil différent dans ces cas. D'un autre côté, l'appelawait
à un contexte à un seul thread tel qu'un répartiteur WPF ou un contexte Winforms devrait être suffisant pour garantir que la poursuite se produit sur l'original. thread