Je comprends à quoi sert le modèle... pour exécuter des tâches longues dans un fil séparé.
Ce n'est absolument pas le but de ce modèle. .
Attendre fait pas mettez l'opération sur un nouveau fil. Assurez-vous que c'est très claire pour vous. Attendre planifie le travail restant comme la continuation de l'opération à latence élevée.
Attendre fait pas transformer une opération synchrone en une opération concurrente asynchrone. Await permet aux programmeurs qui travaillent avec un modèle déjà asynchrone d'écrire leur logique pour ressembler à des flux de travail synchrones. . Await ne crée pas et ne détruit pas l'asynchronisme. gère l'asynchronie existante.
Créer un nouveau fil de discussion, c'est comme embaucher un ouvrier. Lorsque vous attendez une tâche, vous n'engagez pas un travailleur pour effectuer cette tâche. Vous demandez "est-ce que cette tâche est déjà faite ? Si ce n'est pas le cas, rappelez-moi quand elle sera terminée pour que je puisse continuer à faire le travail qui dépend de cette tâche. En attendant, je vais aller travailler sur cette autre chose ici..."
Si vous êtes en train de faire vos impôts et que vous vous rendez compte que vous avez besoin d'un numéro de votre travail, et que le courrier n'est pas encore arrivé, vous n'allez pas engager un travailleur pour attendre près de la boîte aux lettres . Vous notez où vous en êtes dans vos impôts, vous allez faire d'autres choses, et quand le courrier arrive, vous reprenez là où vous vous êtes arrêté. C'est attendre . C'est attente asynchrone d'un résultat .
Cette utilisation excessive de await / async est-elle nécessaire pour le développement web ou pour quelque chose comme Angular ?
C'est pour gérer la latence.
En quoi le fait de rendre chaque ligne asynchrone va-t-il améliorer les performances ?
De deux façons. Tout d'abord, en garantissant que les applications restent réactives dans un monde où les opérations à forte latence sont nombreuses. Ce type de performance est important pour les utilisateurs qui ne veulent pas que leurs applications se bloquent. Deuxièmement, en fournissant aux développeurs des outils pour exprimer les relations de dépendance des données dans les flux de travail asynchrones. En évitant de bloquer les opérations à forte latence, les ressources du système sont libérées pour travailler sur les opérations non bloquées.
Pour moi, ça va tuer les performances en faisant tourner tous ces fils, non ?
Il n'y a pas de fil conducteur. La concurence est un mécanisme permettant de réaliser l'asynchronie, mais ce n'est pas le seul.
Ok, donc si j'écris du code comme : await someMethod1() ; await someMethod2() ; await someMethod3() ; cela va magiquement rendre l'application plus réactive ?
Plus réactif par rapport à quoi ? Par rapport au fait d'appeler ces méthodes sans les attendre ? Non, bien sûr que non. Par rapport à l'attente synchrone de la fin des tâches ? Absolument, oui.
C'est ce que je ne comprends pas, je suppose. Si vous attendez les 3 à la fin, alors oui, vous utilisez les 3 méthodes en parallèle.
Non non non. Arrêtez de penser au parallélisme. Il n'y a pas besoin de parallélisme.
Pensez-y de cette façon. Vous souhaitez préparer un sandwich aux œufs au plat. Vous avez les tâches suivantes :
- Faire frire un œuf
- Faire griller du pain
- Assembler un sandwich
Trois tâches. La troisième tâche dépend des résultats des deux premières, mais les deux premières tâches ne dépendent pas l'une de l'autre. Voici donc des flux de travail :
- Mettez un œuf dans la poêle. Pendant que l'œuf est en train de frire, fixez-le.
- Une fois l'œuf cuit, mettez des toasts dans le grille-pain. Fixez le grille-pain.
- Une fois que le pain grillé est cuit, mettez l'œuf sur le pain grillé.
Le problème est que vous pourriez mettre le toast dans le grille-pain pendant que l'œuf cuit. Flux de travail alternatif :
- Mettez un œuf dans la casserole. Réglez une alarme qui sonne lorsque l'œuf est cuit.
- Mets les toasts dans le grille-pain. Réglez une alarme qui sonne lorsque le toast est prêt.
- Vérifiez votre courrier. Faites vos impôts. Polir l'argenterie. Tout ce que vous devez faire.
- Lorsque les deux alarmes ont sonné, prenez l'œuf et le toast, mettez-les ensemble, et vous avez un sandwich.
Vous voyez pourquoi le flux de travail asynchrone est bien plus efficace ? Vous faites beaucoup de choses pendant que vous attendez que l'opération à forte latence se termine. Mais vous n'avez pas engagé un chef des œufs et un chef des toasts. . Il n'y a pas de nouveaux fils !
Le flux de travail que j'ai proposé serait le suivant :
eggtask = FryEggAsync();
toasttask = MakeToastAsync();
egg = await eggtask;
toast = await toasttask;
return MakeSandwich(egg, toast);
Maintenant, comparez ça à :
eggtask = FryEggAsync();
egg = await eggtask;
toasttask = MakeToastAsync();
toast = await toasttask;
return MakeSandwich(egg, toast);
Vous voyez comment ce flux de travail diffère ? Ce flux de travail est :
- Mettez un œuf dans la casserole et mettez une alarme.
- Va faire un autre travail jusqu'à ce que l'alarme se déclenche.
- Sortez l'œuf de la casserole, mettez le pain dans le grille-pain. Mettez une alarme...
- Va faire un autre travail jusqu'à ce que l'alarme se déclenche.
- Quand l'alarme se déclenche, assemblez le sandwich.
Ce flux de travail est moins efficace parce que nous n'avons pas réussi à capturer le fait que les tâches du toast et de l'œuf ont une latence élevée et sont indépendantes. . Mais c'est sûrement une utilisation plus efficace des ressources que de faire rien pendant que tu attends que l'oeuf cuise.
Le point de tout cela est : les fils sont follement chers, donc Ne le fais pas. créer de nouveaux fils. Plutôt, utiliser plus efficacement le fil de discussion dont vous disposez en le faisant travailler pendant que vous effectuez des opérations à forte latence. . Attendre est pas Il s'agit de réaliser plus de travail sur un seul fil dans un monde où la latence des calculs est élevée.
Peut-être que ce calcul est effectué sur un autre thread, peut-être qu'il est bloqué sur le disque, peu importe. Cela n'a pas d'importance. Le point est, attendre est pour gérer que l'asynchronie, et non créer il.
J'ai du mal à comprendre comment la programmation asynchrone peut être possible sans utiliser le parallélisme quelque part. Par exemple, comment dire au programme de commencer à préparer les toasts en attendant les œufs sans que DoEggs() ne s'exécute simultanément, du moins en interne ?
Reprenons l'analogie. Vous êtes en train de préparer un sandwich aux œufs, les œufs et les toasts sont en train de cuire, et vous commencez à lire votre courrier. Vous en êtes à la moitié du courrier lorsque les œufs sont cuits, alors vous mettez le courrier de côté et retirez l'œuf du feu. Puis vous retournez à votre courrier. Puis les toasts sont cuits et vous faites le sandwich. Puis vous finissez de lire votre courrier après avoir fait le sandwich. Comment avez-vous fait tout cela sans embaucher du personnel, une personne pour lire le courrier, une personne pour faire cuire l'œuf, une pour faire les toasts et une pour assembler le sandwich ? Vous avez fait tout cela avec un seul ouvrier.
Comment avez-vous fait cela ? En divisant les tâches en petits morceaux, en notant quels morceaux doivent être faits dans quel ordre, et puis coopérativement multitâche les pièces.
Les jeunes d'aujourd'hui, avec leurs grands modèles de mémoire virtuelle plate et leurs processus multithreads, pensent qu'il en a toujours été ainsi, mais ma mémoire remonte à l'époque de Windows 3, qui n'avait rien de tout cela. Si vous vouliez que deux choses se produisent "en parallèle", c'est ce que vous faisiez : vous divisiez les tâches en petites parties et vous vous relayiez pour les exécuter. L'ensemble du système d'exploitation était basé sur ce concept.
Maintenant, vous pourriez regarder l'analogie et dire "OK, mais une partie du travail, comme faire griller les toasts, est effectuée par une machine", et que est la source du parallélisme. Bien sûr, je n'ai pas eu besoin d'engager un ouvrier pour griller le pain, mais j'ai obtenu le parallélisme dans le matériel. Et c'est la bonne façon de voir les choses. Le parallélisme matériel et le parallélisme des fils sont différents. . Lorsque vous faites une demande asynchrone au sous-système réseau pour aller chercher un enregistrement dans une base de données, il y a pas de fil qui reste là à attendre le résultat. Le matériel atteint un parallélisme à un niveau très, très inférieur à celui des threads du système d'exploitation.
Si vous voulez une explication plus détaillée de la façon dont le matériel fonctionne avec le système d'exploitation pour obtenir l'asynchronie, lisez " Il n'y a pas de fil conducteur " par Stephen Cleary.
Ainsi, lorsque vous voyez "asynchrone", ne pensez pas "parallèle". Pensez plutôt à une opération à forte latence divisée en petits morceaux. beaucoup de de telles opérations dont les pièces ne dépendent pas les unes des autres, alors vous pouvez s'entrelacer de manière coopérative l'exécution de ces pièces sur un seul fil.
Comme vous pouvez l'imaginer, c'est très difficile d'écrire des flux de contrôle qui vous permettent d'abandonner ce que vous êtes en train de faire, de faire autre chose, et de reprendre sans problème là où vous vous êtes arrêté. C'est pourquoi nous faisons faire ce travail par le compilateur ! L'intérêt de "await" est qu'il vous permet de gérer ces flux de travail asynchrones en les décrivant comme des flux de travail synchrones. Chaque fois qu'il y a un point où vous pouvez mettre cette tâche de côté et y revenir plus tard, écrivez "await". Le compilateur se chargera de transformer votre code en de nombreux petits morceaux qui pourront chacun être planifiés dans un flux de travail asynchrone.
UPDATE :
Dans votre dernier exemple, quelle serait la différence entre
eggtask = FryEggAsync();
egg = await eggtask;
toasttask = MakeToastAsync();
toast = await toasttask;
egg = await FryEggAsync();
toast = await MakeToastAsync();?
Je suppose qu'il les appelle de manière synchrone mais les exécute de manière asynchrone ? Je dois admettre que je n'ai jamais pris la peine d'attendre les tâches séparément auparavant.
Il n'y a pas de différence.
Lorsque FryEggAsync
est appelé, il est appelé que ce soit await
apparaît devant elle ou non. await
est un opérateur . Il opère sur la chose a retourné de l'appel à FryEggAsync
. C'est comme n'importe quel autre opérateur.
Laissez-moi le dire encore une fois : await
est un opérateur et son opérande est une tâche. C'est un opérateur très inhabituel, certes, mais grammaticalement c'est un opérateur, et il opère sur un valeur comme n'importe quel autre opérateur.
Laissez-moi le dire encore une fois : await
n'est pas une poussière magique que l'on met sur un site d'appel et qui, soudainement, est déplacé vers un autre thread. L'appel se produit quand l'appel se produit, l'appel renvoie un valeur et cette valeur est une référence à un objet qui est un opérande légal de la fonction await
opérateur .
Alors oui,
var x = Foo();
var y = await x;
et
var y = await Foo();
sont la même chose, la même chose que
var x = Foo();
var y = 1 + x;
et
var y = 1 + Foo();
sont la même chose.
Alors revoyons tout ça encore une fois, parce que vous semblez croire au mythe selon lequel await
causes asynchronie. Ce n'est pas le cas.
async Task M() {
var eggtask = FryEggAsync();
Supposons que M()
s'appelle. FryEggAsync
est appelé. De manière synchrone. Il n'existe pas d'appel asynchrone ; vous voyez un appel, le contrôle passe à l'appelé jusqu'à ce que l'appelé revienne. Le destinataire retourne une tâche qui représente un œuf à mettre à disposition dans le futur .
Comment FryEggAsync
faire ça ? Je ne sais pas et je m'en fiche. Tout ce que je sais, c'est que je l'appelle, et que je reçois en retour un objet qui représente une valeur future. Peut-être que cette valeur est produite sur un autre thread. Peut-être qu'elle est produite sur ce fil mais à l'avenir . Il est peut-être produit par un matériel à usage spécifique, comme un contrôleur de disque ou une carte réseau. Je m'en moque. Ce qui m'importe, c'est que je récupère une tâche.
egg = await eggtask;
Maintenant, nous prenons cette tâche et await
lui demande "avez-vous terminé ?" Si la réponse est oui, alors egg
se voit attribuer la valeur produite par la tâche. Si la réponse est négative, alors M()
renvoie un Task
représentant "le travail de M sera achevé dans le futur". Le reste de M() est signé comme la suite de eggtask
donc lorsque eggtask
est terminée, elle appelle M()
encore une fois et le ramasser pas depuis le début mais de l'affectation à egg
. M() est un reprenable à tout moment méthode. Le compilateur fait la magie nécessaire pour que cela se produise.
Alors maintenant, nous sommes de retour. Le fil continue à faire ce qu'il fait. A un moment donné, l'oeuf est prêt, donc la suite de l'action de eggtask
est invoqué, ce qui entraîne M()
pour être appelé à nouveau. Il reprend au point où il s'est arrêté : il attribue l'œuf qui vient d'être produit à egg
. Et maintenant, nous continuons notre route :
toasttask = MakeToastAsync();
Encore une fois, l'appel renvoie une tâche, et nous :
toast = await toasttask;
vérifier si la tâche est terminée. Si oui, nous attribuons toast
. Si non, alors nous revenons de M() à nouveau et le continuation de toasttask
est *le reste de M().
Et ainsi de suite.
Élimination de la task
Les variables ne font rien d'utile. Le stockage des valeurs est alloué ; on ne lui donne simplement pas de nom.
UNE AUTRE MISE À JOUR :
Est-il possible d'appeler les méthodes retournant des tâches le plus tôt possible et de les attendre le plus tard possible ?
L'exemple donné est le suivant :
var task = FooAsync();
DoSomethingElse();
var foo = await task;
...
Il y a un peu de Il y a des arguments à faire valoir pour cela. Mais prenons un peu de recul. Le but de la await
L'opérateur est de construire un flux de travail asynchrone en utilisant les conventions de codage d'un flux de travail synchrone . Donc la chose à laquelle il faut penser est quel est ce flux de travail ? A flux de travail impose un ordre à un ensemble de tâches connexes.
La manière la plus simple de voir l'ordre requis dans un flux de travail est d'examiner le fichier dépendance des données . Vous ne pouvez pas faire le sandwich avant que le pain grillé ne sorte du grille-pain, donc vous devez obtenir le pain grillé. quelque part . Puisque await extrait la valeur de la tâche terminée, il doit y avoir un await quelque part entre la création de la tâche du grille-pain et la création du sandwich.
Vous pouvez également représenter les dépendances sur les effets secondaires. Par exemple, l'utilisateur appuie sur le bouton, vous voulez donc faire jouer le son de la sirène, puis attendre trois secondes, puis ouvrir la porte, puis attendre trois secondes, puis fermer la porte :
DisableButton();
PlaySiren();
await Task.Delay(3000);
OpenDoor();
await Task.Delay(3000);
CloseDoor();
EnableButton();
Cela n'aurait aucun sens de dire
DisableButton();
PlaySiren();
var delay1 = Task.Delay(3000);
OpenDoor();
var delay2 = Task.Delay(3000);
CloseDoor();
EnableButton();
await delay1;
await delay2;
Parce que ce n'est pas le flux de travail souhaité.
Ainsi, la réponse à votre question est la suivante : différer l'attente jusqu'au moment où la valeur est réellement nécessaire est une assez bonne pratique, car cela augmente les possibilités de programmer le travail de manière efficace. Mais vous pouvez aller trop loin ; assurez-vous que le flux de travail qui est mis en œuvre est celui que vous voulez.