Asynchrone n'implique pas Parallèle
Asynchrone n'implique que la concurrence. En fait, même l'utilisation de threads explicites ne garantit pas qu'ils s'exécuteront simultanément (par exemple, lorsque les threads ont une affinité pour le même noyau unique, ou plus communément lorsqu'il n'y a qu'un seul noyau dans la machine au départ).
Par conséquent, vous ne devez pas vous attendre à ce qu'une opération asynchrone se produise en même temps que quelque chose d'autre. Asynchrone signifie seulement qu'elle se produira, éventuellement à un autre moment ( a (grec) = sans, syn (grec) = ensemble, khronos (grec) = temps. => Asynchrone \= ne se produisant pas au même moment).
Note : L'idée de l'asynchronisme est que lors de l'invocation, vous ne vous souciez pas du moment où le code sera effectivement exécuté. Cela permet au système de tirer parti du parallélisme, si possible, pour exécuter l'opération. Elle peut même être exécutée immédiatement. Cela peut même se produire sur le même thread... nous y reviendrons plus tard.
Quand vous await
l'opération asynchrone, vous créez des Concurrence ( com (latin) = ensemble, current (latin) = courir. => "Concurrent" = de courir ensemble ). C'est parce que vous demandez que l'opération asynchrone soit terminée avant de passer à autre chose. On peut dire que l'exécution converge. Ce concept est similaire à celui de la jonction des threads.
Quand l'asynchrone ne peut pas être Parallèle
Lorsque vous utilisez async/await, il n'y a aucune garantie que la méthode que vous appelez lorsque vous faites await FooAsync() s'exécutera réellement de manière asynchrone. L'implémentation interne est libre de retourner en utilisant un chemin complètement synchrone.
Cela peut se produire de trois façons :
-
Il est possible d'utiliser await
sur tout ce qui renvoie Task
. Lorsque vous recevez le Task
elle pourrait avoir déjà été réalisée.
Pourtant, cela ne signifie pas pour autant qu'elle a fonctionné de manière synchrone. En fait, cela suggère qu'il s'est exécuté de manière asynchrone et qu'il s'est terminé avant que vous n'obteniez l'icône Task
instance.
Gardez à l'esprit que vous pouvez await
sur une tâche déjà accomplie :
private static async Task CallFooAsync()
{
await FooAsync();
}
private static Task FooAsync()
{
return Task.CompletedTask;
}
private static void Main()
{
CallFooAsync().Wait();
}
En outre, si un async
n'a pas de await
il fonctionnera de manière synchrone.
Remarque : Comme vous le savez déjà, une méthode qui renvoie un fichier Task
peut être en attente sur le réseau, ou sur le système de fichiers, etc le fait de le faire n'implique pas de démarrer une nouvelle Thread
ou mettre en file d'attente quelque chose sur le ThreadPool
.
-
Dans un contexte de synchronisation géré par un seul thread, le résultat sera d'exécuter la commande Task
de manière synchrone, avec une certaine surcharge. C'est le cas du thread de l'interface utilisateur, je parlerai plus en détail de ce qui se passe ci-dessous.
-
Il est possible d'écrire un TaskScheduler pour toujours exécuter les tâches de manière synchrone. Sur le même thread, qui effectue l'invocation.
Note : récemment, j'ai écrit un SyncrhonizationContext
qui exécute les tâches sur un seul fil. Vous pouvez le trouver à l'adresse suivante Création d'un planificateur de tâches (System.Threading.Tasks.) . Il en résulterait que TaskScheduler
avec un appel à FromCurrentSynchronizationContext
.
La valeur par défaut TaskScheduler
mettra en file d'attente les invocations à la fonction ThreadPool
. Pourtant, lorsque vous attendez sur l'opération, si elle n'a pas été exécutée sur l'ordinateur de l'entreprise, elle ne l'est pas. ThreadPool
il essaiera de le retirer de la ThreadPool
et l'exécuter en ligne (sur le même thread qui attend... le thread attend de toute façon, donc il n'est pas occupé).
Note : Une exception notable est un Task
marqué avec LongRunning
. LongRunning
Task
s s'exécuteront sur un thread séparé .
Votre question
Si j'ai une tâche qui s'exécute longtemps et qui est liée au CPU (disons qu'elle fait beaucoup de calculs difficiles), alors l'exécution asynchrone de cette tâche doit bloquer un thread, n'est-ce pas ? Quelque chose doit en effet effectuer les calculs. Si je l'attends, un thread est bloqué.
Si vous effectuez des calculs, ils doivent avoir lieu sur un fil, cette partie est juste.
Pourtant, la beauté de async
y await
est que le fil d'attente ne doit pas être bloqué (nous y reviendrons plus tard). Pourtant, il est très facile de se tirer une balle dans le pied en programmant l'exécution de la tâche attendue sur le même thread que celui qui attend, ce qui entraîne une exécution synchrone (ce qui est une erreur facile dans le thread de l'interface utilisateur).
L'une des principales caractéristiques de async
y await
est qu'ils prennent le SynchronizationContext
de l'appelant. Pour la plupart des fils, cela se traduit par l'utilisation de l'option par défaut TaskScheduler
(qui, comme mentionné plus haut, utilise le ThreasPool
). Cependant, pour le thread de l'interface utilisateur, il s'agit de poster les tâches dans la file d'attente des messages, ce qui signifie qu'elles seront exécutées sur le thread de l'interface utilisateur. L'avantage de cette méthode est que vous n'avez pas besoin d'utiliser la fonction Invoke
ou BeginInvoke
pour accéder aux composants de l'interface utilisateur.
Avant d'entrer dans le détail de comment await
a Task
depuis le thread de l'interface utilisateur sans le bloquer, je tiens à souligner qu'il est possible d'implémenter une fonction TaskScheduler
où si vous await
sur un Task
vous ne bloquez pas votre fil de discussion ou ne le mettez pas en veilleuse, mais vous laissez votre fil en choisir un autre. Task
qui est en attente d'exécution. Quand j'étais backporting de Tasks for .NET 2.0 Je l'ai expérimenté.
Quel est l'exemple d'une méthode véritablement asynchrone et comment fonctionne-t-elle réellement ? Sont-elles limitées aux opérations d'E/S qui tirent parti de certaines capacités matérielles, de sorte qu'aucun thread n'est jamais bloqué ?
Vous semblez confondre asynchrone con ne pas bloquer un fil . Si vous voulez un exemple d'opérations asynchrones dans .NET qui ne nécessitent pas le blocage d'un thread, une façon de procéder que vous trouverez peut-être facile à comprendre est d'utiliser continuations au lieu de await
. Et pour les continuations que vous devez exécuter sur le thread de l'interface utilisateur, vous pouvez utiliser TaskScheduler.FromCurrentSynchronizationContext
.
Ne mettez pas en place une attente fantaisiste . Et par là, je veux dire utiliser un Timer
, Application.Idle
ou quelque chose comme ça.
Lorsque vous utilisez async
vous dites au compilateur de réécrire le code de la méthode d'une manière qui permet de la casser. Le résultat est similaire aux continuations, avec une syntaxe beaucoup plus pratique. Lorsque le thread atteint un await
le site Task
sera programmée, et le fil de discussion est libre de continuer après l'exécution de l'opération en cours. async
invocation (hors de la méthode). Lorsque le Task
est fait, la suite (après le await
) est programmé.
Pour le thread UI, cela signifie qu'une fois qu'il atteint await
il est libre de continuer à traiter les messages. Une fois que le message attendu Task
est fait, la suite (après le await
) sera programmé. Par conséquent, atteindre await
n'implique pas de bloquer le fil.
Pourtant, en ajoutant aveuglément async
y await
ne résoudra pas tous vos problèmes.
Je vous soumets une expérience. Prenez une nouvelle application Windows Forms, déposez-y une Button
et un TextBox
et ajoutez le code suivant :
private async void button1_Click(object sender, EventArgs e)
{
await WorkAsync(5000);
textBox1.Text = @"DONE";
}
private async Task WorkAsync(int milliseconds)
{
Thread.Sleep(milliseconds);
}
Cela bloque l'interface utilisateur. Ce qui se passe, c'est que, comme mentionné plus tôt, await
utilise automatiquement le SynchronizationContext
du fil de l'appelant. Dans ce cas, il s'agit du thread de l'interface utilisateur. Par conséquent, WorkAsync
s'exécutera sur le fil d'exécution de l'interface utilisateur.
C'est ce qui se passe :
- Les threads de l'interface utilisateur reçoivent le message de clic et appellent le gestionnaire d'événement de clic.
- Dans le gestionnaire d'événement de clic, le thread UI atteint
await WorkAsync(5000)
-
WorkAsync(5000)
(et la planification de sa suite) est programmée pour s'exécuter sur le contexte de synchronisation actuel, qui est le contexte de synchronisation du thread de l'interface utilisateur ce qui signifie qu'elle affiche un message pour l'exécuter
- Le thread UI est maintenant libre de traiter d'autres messages.
- Le thread de l'interface utilisateur choisit le message à exécuter.
WorkAsync(5000)
et de programmer sa poursuite
- Le thread de l'interface utilisateur appelle
WorkAsync(5000)
avec continuation
- Sur
WorkAsync
l'interface utilisateur fonctionne Thread.Sleep
. L'interface utilisateur est maintenant irresponsable pendant 5 secondes.
- La suite planifie l'exécution du reste du gestionnaire de l'événement de clic, ce qui est fait en postant un autre message pour le thread de l'interface utilisateur.
- Le thread UI est maintenant libre de traiter d'autres messages.
- Le thread de l'interface utilisateur choisit le message à poursuivre dans le gestionnaire de l'événement de clic.
- Le thread UI met à jour la zone de texte
Le résultat est une exécution synchrone, avec une surcharge.
Oui, vous devez utiliser Task.Delay
au lieu de cela. Ce n'est pas la question ; considérez Sleep
un substitut de calcul. Le fait est que le simple fait d'utiliser async
y await
partout ne vous donnera pas une application qui est automatiquement parallèle. Il est bien mieux de choisir ce que vous voulez exécuter sur un thread d'arrière-plan (par exemple, sur l'application ThreadPool
) et ce que vous voulez exécuter sur le thread de l'interface utilisateur.
Maintenant, essayez le code suivant :
private async void button1_Click(object sender, EventArgs e)
{
await Task.Run(() => Work(5000));
textBox1.Text = @"DONE";
}
private void Work(int milliseconds)
{
Thread.Sleep(milliseconds);
}
Vous constaterez que l'attente ne bloque pas l'interface utilisateur. C'est parce que dans ce cas Thread.Sleep
est maintenant en cours d'exécution sur le ThreadPool
grâce à Task.Run
. Et grâce à button1_Click
être async
une fois que le code atteint await
le thread UI est libre de continuer à travailler. Après le Task
est fait, le code reprendra après le await
grâce au compilateur qui a réécrit la méthode pour permettre précisément cela.
C'est ce qui se passe :
- Les threads de l'interface utilisateur reçoivent le message de clic et appellent le gestionnaire d'événement de clic.
- Dans le gestionnaire d'événement de clic, le thread UI atteint
await Task.Run(() => Work(5000))
-
Task.Run(() => Work(5000))
(et la planification de sa suite) est programmée pour s'exécuter sur le contexte de synchronisation actuel, qui est le contexte de synchronisation du thread de l'interface utilisateur ce qui signifie qu'elle affiche un message pour l'exécuter
- Le thread UI est maintenant libre de traiter d'autres messages.
- Le thread de l'interface utilisateur choisit le message à exécuter.
Task.Run(() => Work(5000))
et programmer sa continuation quand elle sera terminée
- Le thread de l'interface utilisateur appelle
Task.Run(() => Work(5000))
avec la suite, cela fonctionnera sur le ThreadPool
- Le thread UI est maintenant libre de traiter d'autres messages.
Lorsque le ThreadPool
Une fois que l'événement est terminé, la continuation planifie l'exécution du reste du gestionnaire de l'événement de clic, en envoyant un autre message au thread de l'interface utilisateur. Lorsque le thread de l'interface utilisateur sélectionne le message à poursuivre dans le gestionnaire d'événement de clic, il met à jour la zone de texte.
22 votes
La lecture obligatoire : blog.stephencleary.com/2013/11/il-n'y-a-pas-de-fil.html
0 votes
J'aime votre question, mais le titre "Méthodes asynchrones en .Net/C#" est trop général.
0 votes
@Julian J'ai mis à jour la question de l'OP pour être plus spécifique.
4 votes
Si c'est lié au CPU, alors le CPU fonctionne sur un thread, oui - tout code fonctionne sur un thread. S'il attend un paquet du réseau, ce n'est pas un code et n'a donc pas besoin d'être exécuté sur un fil. (Il y a du code quelque part pour commencer à attendre et arrêter d'attendre, mais l'attente réelle n'est pas du code).