La réponse de Stephen est déjà excellente, je ne vais donc pas répéter ce qu'il a dit ; j'ai déjà eu l'occasion de répéter les mêmes arguments de nombreuses fois sur Stack Overflow (et ailleurs).
Au lieu de cela, laissez-moi me concentrer sur une chose abstraite importante à propos du code asynchrone : ce n'est pas un qualificatif absolu. Il est inutile de dire qu'un morceau de code est asynchrone - il est toujours asynchrone. par rapport à quelque chose d'autre . C'est très important.
Le but de await
consiste à construire des flux de travail synchrones par-dessus des opérations asynchrones et un code synchrone de connexion. Votre code apparaît parfaitement synchrone 1 au code lui-même.
var a = await A();
await B(a);
L'ordre des événements est spécifié par l'option await
invocations. B utilise la valeur de retour de A, ce qui signifie que A doit avoir été exécutée avant B. La méthode contenant ce code a un flux synchrone, et les deux méthodes A et B sont synchrones l'une par rapport à l'autre.
C'est très utile, car il est généralement plus facile de penser aux flux de travail synchrones et, plus important encore, beaucoup de flux de travail sont tout simplement sont synchrone. Si B a besoin du résultat de A pour s'exécuter, il doit courir après A 2 . Si vous devez effectuer une requête HTTP pour obtenir l'URL d'une autre requête HTTP, vous devez attendre que la première requête soit terminée ; cela n'a rien à voir avec la planification des threads/tâches. Nous pourrions peut-être appeler cela la "synchronicité inhérente", en dehors de la "synchronicité accidentelle", qui consiste à imposer un ordre à des choses qui n'ont pas besoin d'être ordonnées.
Vous dites :
Dans mon esprit, puisque je fais principalement du développement d'interface utilisateur, le code asynchrone est un code qui ne s'exécute pas sur le thread de l'interface utilisateur, mais sur un autre thread.
Vous décrivez un code qui s'exécute de manière asynchrone par rapport à l'interface utilisateur. C'est certainement un cas très utile pour l'asynchronie (les gens n'aiment pas les interfaces utilisateur qui cessent de répondre). Mais ce n'est qu'un cas spécifique d'un principe plus général - permettre aux choses de se produire dans le désordre les unes par rapport aux autres. Encore une fois, il ne s'agit pas d'un absolu - vous voulez un peu de de se produire dans le désordre (par exemple, lorsque l'utilisateur fait glisser la fenêtre ou que la barre de progression change, la fenêtre doit toujours se redessiner), tandis que d'autres doivent pas se produisent dans le désordre (le bouton Traiter ne doit pas être cliqué avant la fin de l'action Charger). await
dans ce cas d'utilisation n'est pas que différent de l'utilisation Application.DoEvents
en principe - elle présente un grand nombre des mêmes problèmes et avantages.
C'est aussi la partie où la citation originale devient intéressante. L'interface utilisateur a besoin d'un fil pour être mise à jour. Ce thread invoque un gestionnaire d'événement, qui peut utiliser await
. Cela signifie-t-il que la ligne où await
est utilisée pour permettre à l'interface utilisateur de se mettre à jour en réponse à l'entrée de l'utilisateur ? Non.
Tout d'abord, vous devez comprendre que await
utilise son argument, comme s'il s'agissait d'un appel de méthode. Dans mon exemple, A
doit avoir déjà été invoqué avant que le code généré par await
peut faire n'importe quoi, y compris "rendre le contrôle à la boucle de l'interface utilisateur". La valeur de retour de A
est Task<T>
au lieu de simplement T
représentant une "valeur possible dans le futur" - et await
-Le code généré vérifie si la valeur est déjà présente (dans ce cas, il continue sur le même thread) ou non (ce qui signifie que nous devons libérer le thread et le renvoyer à la boucle de l'interface utilisateur). Mais dans les deux cas, le code Task<T>
la valeur elle-même doit ont été renvoyés de A
.
Considérez cette mise en œuvre :
public async Task<int> A()
{
Thread.Sleep(1000);
return 42;
}
L'appelant doit A
pour retourner une valeur (une tâche de int) ; puisqu'il n'y a pas de await
dans la méthode, cela signifie que le return 42;
. Mais cela ne peut pas se produire avant la fin du sommeil, car les deux opérations sont synchrones par rapport au thread. Le thread appelant sera bloqué pendant une seconde, qu'il utilise ou non la commande await
ou non - le blocage est dans A()
elle-même, pas await theTaskResultOfA
.
En revanche, considérez ceci :
public async Task<int> A()
{
await Task.Delay(1000);
return 42;
}
Dès que l'exécution arrive à la await
il s'aperçoit que la tâche en attente n'est pas encore terminée et renvoie le contrôle à son appelant. await
dans l'appelant renvoie donc le contrôle à son appelant. Nous avons réussi à rendre une partie du code asynchrone par rapport à l'interface utilisateur. Le synchronisme entre le thread UI et A était accidentel, et nous l'avons supprimé.
L'important ici est qu'il n'y a aucun moyen de distinguer les deux implémentations de l'extérieur sans inspecter le code. Seul le type de retour fait partie de la signature de la méthode - il n'est pas dit que la méthode s'exécutera de manière asynchrone, seulement qu'elle mai . Cela peut être dû à un grand nombre de bonnes raisons, il est donc inutile de s'y opposer - par exemple, il est inutile de rompre le fil d'exécution lorsque le résultat est déjà disponible :
var responseTask = GetAsync("http://www.google.com");
// Do some CPU intensive task
ComputeAllTheFuzz();
response = await responseTask;
Nous devons faire des travaux. Certains événements peuvent s'exécuter de manière asynchrone par rapport à d'autres (dans ce cas, ComputeAllTheFuzz
est indépendant de la requête HTTP) et sont asynchrones. Mais à un moment donné, nous devons revenir à un flux de travail synchrone (par exemple, quelque chose qui nécessite à la fois le résultat de la requête HTTP et l'exécution de l'action). ComputeAllTheFuzz
et la requête HTTP). C'est le await
qui synchronise à nouveau l'exécution (si vous aviez plusieurs flux de travail asynchrones, vous utiliseriez quelque chose du type Task.WhenAll
). Cependant, si la requête HTTP a réussi à se terminer avant le calcul, il est inutile de relâcher le contrôle à l'instant où la requête HTTP se termine. await
point - nous pouvons simplement continuer sur le même fil. Il n'y a pas eu de gaspillage du CPU - pas de blocage du thread ; il effectue un travail utile pour le CPU. Mais nous n'avons donné aucune opportunité à l'interface utilisateur de se mettre à jour.
C'est bien sûr la raison pour laquelle ce modèle est généralement évité dans les méthodes asynchrones plus générales. Il est utile pour certaines utilisations du code asynchrone (éviter de gaspiller des threads et du temps CPU), mais pas pour d'autres (garder l'interface utilisateur réactive). Si vous attendez d'une telle méthode qu'elle maintienne la réactivité de l'interface utilisateur, vous ne serez pas satisfait du résultat. Mais si vous l'utilisez dans le cadre d'un service web, par exemple, cela fonctionnera parfaitement - l'objectif est d'éviter de gaspiller des threads, et non de garder l'interface utilisateur réactive (cela est déjà assuré par l'invocation asynchrone du point de terminaison du service - il n'y a aucun avantage à refaire la même chose du côté du service).
En bref, await
vous permet d'écrire du code qui est asynchrone par rapport à son appelant. Il n'invoque pas un pouvoir magique d'asynchronicité, il n'est pas asynchrone par rapport à tout, il ne vous empêche pas d'utiliser le CPU ou de bloquer des threads. Il vous donne simplement les outils nécessaires pour créer facilement un flux de travail synchrone à partir d'opérations asynchrones, et présenter une partie de l'ensemble du flux de travail comme étant asynchrone par rapport à son appelant.
Considérons un gestionnaire d'événements de l'interface utilisateur. Si les opérations asynchrones individuelles n'ont pas besoin d'un thread pour s'exécuter (par exemple, les E/S asynchrones), une partie de la méthode asynchrone peut permettre à d'autres codes de s'exécuter sur le thread original (et l'interface utilisateur reste réactive dans ces parties). Lorsque l'opération nécessite à nouveau le CPU/thread, elle peut ou non exiger l'utilisation de la méthode asynchrone. original pour continuer le travail. Si c'est le cas, l'interface utilisateur sera à nouveau bloquée pour la durée du travail de l'unité centrale ; si ce n'est pas le cas (la fonction attendez le précise en utilisant ConfigureAwait(false)
), le code de l'interface utilisateur sera exécuté en parallèle. En supposant qu'il y ait suffisamment de ressources pour gérer les deux, bien sûr. Si vous avez besoin que l'interface utilisateur reste réactive à tout moment, vous ne pouvez pas utiliser le thread de l'interface utilisateur pour une exécution suffisamment longue pour être perceptible - même si cela signifie que vous devez envelopper une méthode asynchrone peu fiable "généralement asynchrone, mais qui bloque parfois pendant quelques secondes" dans un thread de type Task.Run
. Il y a des coûts et des avantages dans les deux approches - c'est un compromis, comme dans toute ingénierie :)
- Bien sûr, c'est parfait dans la mesure où l'abstraction tient la route - toute abstraction a des fuites, et il y a beaucoup de fuites dans await et d'autres approches de l'exécution asynchrone.
- Un optimiseur suffisamment intelligent pourrait permettre à une partie de B de s'exécuter, jusqu'au point où la valeur de retour de A est réellement nécessaire ; c'est ce que votre CPU fait avec du code "synchrone" normal ( Exécution hors service ). De telles optimisations doit préserver l'apparence de la synchronisation, cependant - si l'unité centrale se trompe dans l'ordre des opérations, elle doit rejeter les résultats et présenter un ordre correct.
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).