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 sais pas vraiment comment expliquer ce qui se passe dans le code suivant:
async void MyThread()
{
while (!_quit)
{
await GetWorkAsync();
}
}
GetWorkAsync()
est supposé renvoyer un Task
attendu 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 continuation) 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 modèle de code while (!_quit) { ... }
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.
Vous pouvez le vérifier sur Essayez Roslyn . Votre méthode d'attente est réécrite dans void IAsyncStateMachine.MoveNext()
sur la classe asynchrone générée.
Ce que vous verrez est quelque chose comme ceci:
if (this.state != 0)
goto label_2;
//set up the state machine here
label_1:
taskAwaiter.GetResult();
taskAwaiter = default(TaskAwaiter);
label_2:
if (!OuterClass._quit)
{
taskAwaiter = GetWorkAsync().GetAwaiter();
//state machine stuff here
}
goto label_1;
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.
Premièrement, Servy a écrit du code dans une réponse à une question similaire, sur laquelle cette réponse est basée:
https://stackoverflow.com/questions/22049339/how-to-create-a-cancellable-task-loop
La réponse de Servy comprend une boucle ContinueWith()
similaire utilisant des constructions TPL sans utilisation explicite des mots clés async
et await
; donc pour répondre à votre question, réfléchissez à ce à quoi pourrait ressembler votre code lorsque votre boucle est déroulée à l'aide de ContinueWith()
private static Task GetWorkWhileNotQuit()
{
var tcs = new TaskCompletionSource<bool>();
Task previous = Task.FromResult(_quit);
Action<Task> continuation = null;
continuation = t =>
{
if (!_quit)
{
previous = previous.ContinueWith(_ => GetWorkAsync())
.Unwrap()
.ContinueWith(_ => previous.ContinueWith(continuation));
}
else
{
tcs.SetResult(_quit);
}
};
previous.ContinueWith(continuation);
return tcs.Task;
}
Cela prend un certain temps pour vous envelopper, mais en résumé:
continuation
représente une fermeture pour la "itération actuelle" previous
représente le Task
contenant l'état de la "itération précédente" (c'est-à-dire qu'il sait quand l '"itération" est terminé et est utilisé pour démarrer le suivant ..)GetWorkAsync()
renvoie un Task
, cela signifie que ContinueWith(_ => GetWorkAsync())
renverra un Task<Task>
D'où l'appel à Unwrap()
pour obtenir le ' tâche intérieure '(c'est-à-dire le résultat réel de GetWorkAsync()
).Donc:
Task.FromResult(_quit)
- son état commence comme Task.Completed == true
.continuation
est exécuté pour la première fois en utilisant previous.ContinueWith(continuation)
continuation
fermeture met à jour previous
pour refléter l'état d'achèvement de _ => GetWorkAsync()
_ => GetWorkAsync()
est terminée, elle "continue avec" _previous.ContinueWith(continuation)
- c'est-à-dire en appelant à nouveau le continuation
lambda previous
a été mis à jour avec l'état de _ => GetWorkAsync()
donc le continuation
lambda est appelé lorsque GetWorkAsync()
revient.Le continuation
lambda vérifie toujours l'état de _quit
Donc, si _quit == false
Alors il n'y a plus de continuations, et le TaskCompletionSource
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 clé async
/await
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 méthode GetWorkAsync()
en ce qui concerne le threading et la sécurité des threads. 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 cela est inattendu, il y a peut-être un .ConfigureAwait
Quelque part?)