Pourquoi les langages de programmation ne gèrent-ils pas automatiquement le problème synchrone / asynchrone?

27

Je n'ai pas trouvé beaucoup de ressources à ce sujet: je me demandais si c'est possible / une bonne idée de pouvoir écrire du code asynchrone de manière synchrone.

Par exemple, voici du code JavaScript qui récupère le nombre d'utilisateurs stockés dans une base de données (une opération asynchrone):

getNbOfUsers(function (nbOfUsers) { console.log(nbOfUsers) });

Ce serait bien de pouvoir écrire quelque chose comme ça:

const nbOfUsers = getNbOfUsers();
console.log(getNbOfUsers);

Et donc le compilateur se chargerait automatiquement d'attendre la réponse, puis de s'exécuter console.log. Il attendra toujours la fin des opérations asynchrones avant que les résultats ne soient utilisés ailleurs. Nous utiliserions tellement moins les promesses de rappel, async / wait ou autre, et n'aurions jamais à nous inquiéter si le résultat d'une opération est disponible immédiatement ou non.

Les erreurs seraient toujours gérables ( nbOfUsersvous avez obtenu un entier ou une erreur?) En utilisant try / catch, ou quelque chose comme des options comme dans le langage Swift .

C'est possible? Ce peut être une idée terrible / une utopie ... Je ne sais pas.

Cinn
la source
58
Je ne comprends pas vraiment votre question. Si vous "attendez toujours l'opération asynchrone", alors ce n'est pas une opération asynchrone, c'est une opération synchrone. Pouvez-vous clarifier? Peut-être donner une spécification du type de comportement que vous recherchez? En outre, «qu'en pensez-vous» est hors sujet sur le génie logiciel . Vous devez formuler votre question dans le contexte d'un problème concret, qui a une réponse unique, sans ambiguïté, canonique et objectivement correcte.
Jörg W Mittag
4
@ JörgWMittag J'imagine un hypothétique C # qui implicitement awaitsa Task<T>pour le convertir enT
Caleth
6
Ce que vous proposez n'est pas réalisable. Ce n'est pas au compilateur de décider si vous voulez attendre le résultat ou peut-être tirer et oublier. Ou exécutez en arrière-plan et attendez plus tard. Pourquoi vous limiter comme ça?
freakish
5
Oui, c'est une terrible idée. Utilisez simplement async/ à la awaitplace, ce qui rend les parties asynchrones de l'exécution explicites.
Bergi
5
Lorsque vous dites que deux choses se produisent simultanément, vous dites que ce n'est pas grave que ces choses se produisent dans n'importe quel ordre. Si votre code n'a aucun moyen de préciser quelles réordonnances ne briseront pas les attentes de votre code, il ne peut pas les rendre simultanées.
Rob

Réponses:

65

Async / Wait est exactement la gestion automatisée que vous proposez, mais avec deux mots-clés supplémentaires. Pourquoi sont-ils importants? Mis à part la compatibilité descendante?

  • Sans points explicites où une coroutine peut être suspendue et reprise, nous aurions besoin d'un système de type pour détecter où une valeur attendue doit être attendue. De nombreux langages de programmation n'ont pas un tel système de type.

  • En rendant explicite l'attente d'une valeur, nous pouvons également transmettre des valeurs attendues comme des objets de première classe: les promesses. Cela peut être très utile lors de l'écriture de code d'ordre supérieur.

  • Le code asynchrone a des effets très profonds sur le modèle d'exécution d'un langage, similaires à l'absence ou à la présence d'exceptions dans le langage. En particulier, une fonction asynchrone ne peut être attendue que par des fonctions asynchrones. Cela affecte toutes les fonctions d'appel! Mais que se passe-t-il si nous changeons une fonction de non asynchrone en async à la fin de cette chaîne de dépendance? Ce serait un changement incompatible vers l'arrière… sauf si toutes les fonctions sont asynchrones et que chaque appel de fonction est attendu par défaut.

    Et cela est hautement indésirable car il a de très mauvaises implications en termes de performances. Vous ne pourriez pas simplement renvoyer des valeurs bon marché. Chaque appel de fonction deviendrait beaucoup plus cher.

L'async est génial, mais une sorte d'async implicite ne fonctionnera pas en réalité.

Les langages fonctionnels purs comme Haskell ont un peu d'échappatoire car l'ordre d'exécution est en grande partie non spécifié et non observable. Ou formulé différemment: tout ordre d'opérations spécifique doit être explicitement codé. Cela peut être assez lourd pour les programmes du monde réel, en particulier pour les programmes lourds d'E / S pour lesquels le code asynchrone convient très bien.

amon
la source
2
Vous n'avez pas nécessairement besoin d'un système de type. Les Futures transparents, par exemple ECMAScript, Smalltalk, Self, Newspeak, Io, Ioke, Seph, peuvent être facilement mis en œuvre sans système de tyoe ni support de langue. Dans Smalltalk et ses descendants, un objet peut changer de façon transparente son identité, dans ECMAScript, il peut changer de manière transparente sa forme. C'est tout ce dont vous avez besoin pour rendre Futures transparent, pas besoin de prise en charge linguistique pour l'asynchronie.
Jörg W Mittag
6
@ JörgWMittag Je comprends ce que vous dites et comment cela pourrait fonctionner, mais des contrats à terme transparents sans système de types font qu'il est assez difficile d'avoir simultanément des contrats à terme de première classe, non? J'aurais besoin d'un moyen pour sélectionner si je veux envoyer des messages à l'avenir ou la valeur de l'avenir, de préférence quelque chose de mieux que someValue ifItIsAFuture [self| self messageIWantToSend]parce que c'est difficile à intégrer avec du code générique.
amon
8
@amon "Je peux écrire mon code asynchrone comme des promesses et les promesses sont des monades." Les monades ne sont pas vraiment nécessaires ici. Thunks sont essentiellement des promesses. Comme presque toutes les valeurs dans Haskell sont encadrées, presque toutes les valeurs dans Haskell sont déjà des promesses. C'est pourquoi vous pouvez lancer à parpeu près n'importe où dans du code Haskell pur et obtenir gratuitement le paralellisme.
DarthFennec
2
Async / Wait me rappelle la monade de continuation.
les
3
En fait, les exceptions et asynchrones / attendent sont des exemples d' effets algébriques .
Alex Reinking
21

Ce qui vous manque, c'est le but des opérations asynchrones: elles vous permettent de profiter de votre temps d'attente!

Si vous transformez une opération asynchrone, comme la demande de certaines ressources d'un serveur, en une opération synchrone en attendant implicitement et immédiatement la réponse, votre thread ne peut rien faire d'autre avec le temps d'attente . Si le serveur met 10 millisecondes pour répondre, il y a environ 30 millions de cycles CPU à perdre. La latence de la réponse devient le temps d'exécution de la demande.

La seule raison pour laquelle les programmeurs ont inventé les opérations asynchrones, est de masquer la latence des tâches intrinsèquement longues derrière d'autres calculs utiles . Si vous pouvez remplir le temps d'attente avec un travail utile, c'est du temps CPU économisé. Si vous ne pouvez pas, eh bien, rien n'est perdu par l'opération asynchrone.

Donc, je recommande d'embrasser les opérations asynchrones que vos langues vous fournissent. Ils sont là pour vous faire gagner du temps.

cmaster
la source
je pensais à un langage fonctionnel où les opérations ne bloquent pas, donc même s'il a une syntaxe synchrone, un calcul de longue durée ne bloquera pas le thread
Cinn
6
@Cinn Je n'ai pas trouvé cela dans la question, et l'exemple dans la question est Javascript, qui n'a pas cette fonctionnalité. Cependant, en général, il est assez difficile pour un compilateur de trouver des opportunités significatives de parallélisation comme vous le décrivez: Une exploitation significative d'une telle fonctionnalité nécessiterait que le programmeur réfléchisse explicitement à ce qu'il a mis juste après un long appel de latence. Si vous rendez le runtime suffisamment intelligent pour éviter cette exigence sur le programmeur, votre runtime réduira probablement les économies de performances car il devra paralléliser agressivement les appels de fonction.
cmaster
2
Tous les ordinateurs attendent à la même vitesse.
Bob Jarvis - Réintègre Monica
2
@BobJarvis Oui. Mais ils diffèrent dans la quantité de travail qu'ils auraient pu faire dans le temps d'attente ...
cmaster
13

Certains le font.

Ils ne sont pas (encore) courants car l'async est une fonctionnalité relativement nouvelle pour laquelle nous venons juste de nous faire une bonne idée si c'est même une bonne fonctionnalité, ou comment la présenter aux programmeurs de manière conviviale / utilisable / expressif / etc. Les fonctionnalités asynchrones existantes sont en grande partie intégrées aux langages existants, ce qui nécessite une approche de conception légèrement différente.

Cela dit, ce n'est pas clairement une bonne idée de faire partout. Un échec courant consiste à effectuer des appels asynchrones en boucle, sérialisant efficacement leur exécution. Le fait d'avoir des appels asynchrones implicites peut masquer ce type d'erreur. De plus, si vous supportez la coercition implicite à partir d'un Task<T>(ou de l'équivalent de votre langue) T, cela peut ajouter un peu de complexité / coût à votre vérificateur de frappe et de rapport d'erreurs lorsqu'il n'est pas clair lequel des deux le programmeur voulait vraiment.

Mais ce ne sont pas des problèmes insurmontables. Si vous vouliez soutenir ce comportement, vous le pourriez presque certainement, mais il y aurait des compromis.

Telastyn
la source
1
Je pense qu'une idée pourrait être de tout emballer dans des fonctions asynchrones, les tâches synchrones se résoudraient immédiatement et nous aurions toutes une sorte à gérer (Edit: @amon a expliqué pourquoi c'était une mauvaise idée ...)
Cinn
8
Pouvez-vous donner quelques exemples pour " Certains le font ", s'il vous plaît?
Bergi
2
La programmation asynchrone n'est en rien nouvelle, c'est juste que de nos jours les gens doivent y faire face plus souvent.
cubique
1
@Cubic - c'est comme une fonction de langue pour autant que je sache. Avant, c'était juste (maladroit) les fonctions de l'espace utilisateur.
Telastyn
12

Il y a des langues qui font ça. Mais, en réalité, il n'y a pas beaucoup de besoin, car il peut être facilement accompli avec les fonctionnalités de langage existantes.

Tant que vous avez un moyen d'exprimer l'asynchronie, vous pouvez implémenter Futures ou Promises uniquement comme une fonctionnalité de bibliothèque, vous n'avez pas besoin de fonctionnalités de langage spéciales. Et aussi longtemps que vous avez certains d'exprimer proxys transparents , vous pouvez mettre les deux fonctions ensemble et vous avez à terme transparents .

Par exemple, dans Smalltalk et ses descendants, un objet peut changer son identité, il peut littéralement «devenir» un objet différent (et en fait la méthode qui fait cela est appelée Object>>become:).

Imaginez un calcul de longue durée qui renvoie a Future<Int>. Cela Future<Int>a toutes les mêmes méthodes que Int, sauf avec des implémentations différentes. Future<Int>La +méthode de n'ajoute pas un autre nombre et renvoie le résultat, elle renvoie un nouveau Future<Int>qui encapsule le calcul. Et ainsi de suite. Les méthodes qui ne peuvent pas être implémentées de manière sensible en renvoyant un Future<Int>, au lieu de cela automatiquement awaitle résultat, puis appellent self become: result., ce qui fera que l'objet en cours d'exécution ( self, c'est-à-dire le Future<Int>) devient littéralement l' resultobjet, c'est-à-dire désormais la référence d'objet qui était auparavant un Future<Int>est maintenant un Intpartout, complètement transparent pour le client.

Aucune fonctionnalité linguistique spéciale liée à l'asynchronie n'est requise.

Jörg W Mittag
la source
Ok, mais qui a des problèmes si les deux Future<T>et Tpartagent une interface commune et j'utiliser la fonctionnalité de cette interface. Doit-il en becomerésulter et ensuite utiliser la fonctionnalité, ou non? Je pense à des choses comme un opérateur d'égalité ou une représentation de débogage sur chaîne.
amon
Je comprends qu'il n'ajoute aucune fonctionnalité, le problème est que nous avons différentes syntaxes pour écrire des calculs de résolution immédiate et des calculs de longue durée, et après cela, nous utiliserions les résultats de la même manière à d'autres fins. Je me demandais si nous pouvions avoir une syntaxe qui gère les deux de manière transparente, la rendant plus lisible et donc le programmeur n'a pas à la gérer. Comme faire a + b, les deux entiers, peu importe si a et b sont disponibles immédiatement ou plus tard, nous écrivons simplement a + b(ce qui rend possible de le faire Int + Future<Int>)
Cinn
@Cinn: Oui, vous pouvez le faire avec Transparent Futures, et vous n'avez pas besoin de fonctionnalités linguistiques spéciales pour le faire. Vous pouvez l'implémenter en utilisant les fonctionnalités déjà existantes, par exemple Smalltalk, Self, Newspeak, Us, Korz, Io, Ioke, Seph, ECMAScript, et apparemment, comme je viens de le lire, Python.
Jörg W Mittag
3
@amon: L'idée de Transparent Futures est que vous ne savez pas que c'est un avenir. De votre point de vue, il n'y a pas d'interface commune entre Future<T>et Tparce que de votre point de vue, il n'y a pasFuture<T> , seulement a T. Maintenant, il y a bien sûr beaucoup de défis d'ingénierie sur la façon de rendre cela efficace, quelles opérations devraient être bloquantes ou non bloquantes, etc., mais cela est vraiment indépendant de si vous le faites en tant que langue ou en tant que fonctionnalité de bibliothèque. La transparence était une exigence stipulée par le PO dans la question, je ne dirai pas qu'elle est difficile et pourrait ne pas avoir de sens.
Jörg W Mittag
3
@ Jörg Cela semble être problématique dans tout sauf les langages fonctionnels puisque vous n'avez aucun moyen de savoir quand le code est réellement exécuté dans ce modèle. Cela fonctionne généralement bien dans Haskell, mais je ne vois pas comment cela fonctionnerait dans des langages plus procéduraux (et même dans Haskell, si vous vous souciez des performances, vous devez parfois forcer une exécution et comprendre le modèle sous-jacent). Une idée intéressante néanmoins.
Voo
7

Ils le font (enfin, la plupart d'entre eux). La fonctionnalité que vous recherchez s'appelle des threads .

Les threads ont cependant leurs propres problèmes:

  1. Parce que le code peut être suspendu à tout moment , vous ne pouvez jamais supposer que les choses ne changeront pas "par elles-mêmes". Lorsque vous programmez avec des threads, vous perdez beaucoup de temps à réfléchir à la façon dont votre programme doit gérer les choses qui changent.

    Imaginez qu'un serveur de jeu traite l'attaque d'un joueur contre un autre joueur. Quelque chose comme ça:

    if (playerInMeleeRange(attacker, victim)) {
        const damage = calculateAttackDamage(attacker, victim);
        if (victim.health <= damage) {
    
            // attacker gets whatever the victim was carrying as loot
            const loot = victim.getInventoryItems();
            attacker.addInventoryItems(loot);
            victim.removeInventoryItems(loot);
    
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon} and you die!");
            victim.setDead();
        } else {
            victim.health -= damage;
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon}!");
        }
        attacker.markAsKiller();
    }
    

    Trois mois plus tard, un joueur découvre qu'en se faisant tuer et en se déconnectant précisément lorsqu'il attacker.addInventoryItemsest en cours d'exécution, il victim.removeInventoryItemséchouera, il pourra conserver ses objets et l'attaquant obtiendra également une copie de ses objets. Il le fait plusieurs fois, créant un million de tonnes d'or à partir de rien et écrasant l'économie du jeu.

    Alternativement, l'attaquant peut se déconnecter pendant que le jeu envoie un message à la victime, et il n'obtiendra pas d'étiquette "meurtrier" au-dessus de sa tête, de sorte que sa prochaine victime ne fuira pas loin de lui.

  2. Étant donné que le code peut être suspendu à tout moment , vous devez utiliser des verrous partout lors de la manipulation des structures de données. J'ai donné un exemple ci-dessus qui a des conséquences évidentes dans un jeu, mais il peut être plus subtil. Pensez à ajouter un élément au début d'une liste chaînée:

    newItem.nextItem = list.firstItem;
    list.firstItem = newItem;
    

    Ce n'est pas un problème si vous dites que les threads ne peuvent être suspendus que lorsqu'ils font des E / S, et à aucun moment. Mais je suis sûr que vous pouvez imaginer une situation où il y a une opération d'E / S - comme la journalisation:

    for (player = playerList.firstItem; player != null; player = item.nextPlayer) {
        debugLog("${item.name} is online, they get a gold star");
        // Oops! The player might've logged out while the log message was being written to disk, and now this will throw an exception and the remaining players won't get their gold stars.
        // Or the list might've been rearranged and some players might get two and some players might get none.
        player.addInventoryItem(InventoryItems.GoldStar);
    }
    
  3. Parce que le code peut être suspendu à tout moment , il pourrait y avoir beaucoup d'état à enregistrer. Le système gère cela en donnant à chaque thread une pile entièrement distincte. Mais la pile est assez grande, vous ne pouvez donc pas avoir plus de 2000 threads dans un programme 32 bits. Ou vous pourriez réduire la taille de la pile, au risque de la rendre trop petite.

user253751
la source
3

Beaucoup de réponses ici trompeuses, car alors que la question portait littéralement sur la programmation asynchrone et non sur les E / S non bloquantes, je ne pense pas que nous puissions en discuter une sans discuter de l'autre dans ce cas particulier.

Alors que la programmation asynchrone est intrinsèquement, eh bien, asynchrone, la raison d'être de la programmation asynchrone est principalement d'éviter de bloquer les threads du noyau. Node.js utilise l'asynchronisme via des rappels ou Promises pour permettre aux opérations de blocage d'être distribuées à partir d'une boucle d'événement et Netty en Java utilise l'asynchronisme via des rappels ou CompletableFutures pour faire quelque chose de similaire.

Cependant, le code non bloquant ne nécessite pas d’asynchronisme . Cela dépend de ce que votre langage de programmation et votre runtime sont prêts à faire pour vous.

Go, Erlang et Haskell / GHC peuvent gérer cela pour vous. Vous pouvez écrire quelque chose comme var response = http.get('example.com/test')et lui faire libérer un thread du noyau dans les coulisses en attendant une réponse. Cela se fait par des goroutines, des processus Erlang ou en forkIOabandonnant les threads du noyau en arrière-plan lors du blocage, ce qui lui permet de faire d'autres choses en attendant une réponse.

Il est vrai que le langage ne peut pas vraiment gérer l'asynchronisme pour vous, mais certaines abstractions vous permettent d'aller plus loin que d'autres, par exemple des continuations non délimitées ou des coroutines asymétriques. Cependant, la cause principale du code asynchrone, le blocage des appels système, peut absolument être écartée du développeur.

Node.js et Java prennent en charge le code non bloquant asynchrone , tandis que Go et Erlang prennent en charge le code non bloquant synchrone . Ce sont deux approches valides avec des compromis différents.

Mon argument plutôt subjectif est que ceux qui s'opposent à la gestion du non-blocage des runtimes au nom du développeur sont comme ceux qui s'opposent à la collecte des ordures au début des années 2000. Oui, cela entraîne un coût (dans ce cas, principalement plus de mémoire), mais cela facilite le développement et le débogage et rend les bases de code plus robustes.

Je dirais personnellement que le code asynchrone non bloquant devrait être réservé à la programmation des systèmes à l'avenir et que les piles technologiques plus modernes devraient migrer vers des exécutions synchrones non bloquantes pour le développement d'applications.

Louis Jackman
la source
1
C'était une réponse vraiment intéressante! Mais je ne suis pas sûr de comprendre votre distinction entre le code non bloquant «synchrone» et «asynchrone». Pour moi, un code synchrone non bloquant signifie quelque chose comme une fonction C comme waitpid(..., WNOHANG)celle-ci qui échoue si elle devait se bloquer. Ou «synchrone» signifie-t-il «il n'y a pas de rappels / machines d'état / boucles d'événements visibles par le programmeur»? Mais pour votre exemple Go, je dois encore attendre explicitement le résultat d'un goroutine en lisant sur une chaîne, non? Comment est-ce moins asynchrone que async / wait dans JS / C # / Python?
amon
1
J'utilise "asynchrone" et "synchrone" pour discuter du modèle de programmation exposé au développeur et "bloquant" et "non bloquant" pour discuter du blocage d'un thread noyau pendant lequel il ne peut rien faire d'utile, même s'il y a d'autres calculs qui doivent être effectués et il existe un processeur logique de rechange qu'il peut utiliser. Eh bien, un goroutine peut simplement attendre un résultat sans bloquer le thread sous-jacent, mais un autre goroutine peut communiquer avec lui sur un canal s'il le souhaite. Le goroutine n'a pas besoin d'utiliser directement un canal pour attendre une lecture de socket non bloquante.
Louis Jackman
Hmm ok, je comprends votre distinction maintenant. Alors que je suis plus préoccupé par la gestion des flux de données et de contrôle entre les coroutines, vous êtes plus soucieux de ne jamais bloquer le thread principal du noyau. Je ne suis pas sûr que Go ou Haskell aient un avantage sur C ++ ou Java à cet égard, car ils peuvent également lancer des threads d'arrière-plan, ce qui nécessite juste un peu plus de code.
amon
@LouisJackman pourrait développer un peu votre dernière déclaration sur le non-blocage asynchrone pour la programmation système. Quels sont les avantages de l'approche non bloquante asynchrone?
sunprophit
@sunprophit Le non-blocage asynchrone n'est qu'une transformation du compilateur (généralement asynchrone / attend), tandis que le non-blocage synchrone nécessite une prise en charge de l'exécution comme une combinaison de manipulation de pile complexe, l'insertion de limites de rendement sur les appels de fonction (qui peuvent entrer en collision avec l'inline), le suivi " réductions »(nécessitant une machine virtuelle comme BEAM), etc. Tout comme la collecte des ordures, elle réduit la complexité d'exécution pour la facilité d'utilisation et la robustesse. Les langages systèmes comme C, C ++ et Rust évitent les fonctionnalités d'exécution plus importantes comme celle-ci en raison de leurs domaines ciblés, donc le non-blocage asynchrone a plus de sens là-bas.
Louis Jackman
2

Si je vous lis bien, vous demandez un modèle de programmation synchrone, mais une implémentation haute performance. Si cela est correct, cela est déjà disponible pour nous sous la forme de fils verts ou de processus comme par exemple Erlang ou Haskell. Alors oui, c'est une excellente idée, mais l'adaptation aux langues existantes ne peut pas toujours être aussi fluide que vous le souhaitez.

monocellule
la source
2

J'apprécie la question et je trouve que la majorité des réponses sont simplement défensives du statu quo. Dans le spectre des langues de bas à haut niveau, nous sommes coincés dans une ornière depuis un certain temps. Le niveau supérieur suivant sera clairement un langage moins axé sur la syntaxe (le besoin de mots clés explicites comme wait et async) et beaucoup plus sur l'intention. (Un crédit évident pour Charles Simonyi, mais en pensant à 2019 et à l'avenir.)

Si j'ai dit à un programmeur d'écrire du code qui récupère simplement une valeur dans une base de données, vous pouvez supposer en toute sécurité que je veux dire "et BTW, ne pas bloquer l'interface utilisateur" et "n'introduisez pas d'autres considérations qui masquent les bogues difficiles à trouver ". Les programmeurs du futur, avec une nouvelle génération de langages et d'outils, seront certainement capables d'écrire du code qui récupère simplement une valeur dans une ligne de code et part de là.

La langue de niveau le plus élevé serait de parler anglais et de compter sur la compétence du responsable de tâche pour savoir ce que vous voulez vraiment faire. (Pensez à l'ordinateur de Star Trek ou demandez quelque chose à Alexa.) Nous en sommes loin, mais nous nous rapprochons de plus en plus, et je m'attends à ce que le langage / compilateur soit davantage capable de générer du code robuste et intentionné sans aller jusqu'à besoin d'IA.

D'une part, il existe de nouveaux langages visuels, comme Scratch, qui font cela et ne sont pas embourbés avec toutes les technicités syntaxiques. Certes, il y a beaucoup de travail en coulisse afin que le programmeur n'ait pas à s'en soucier. Cela dit, je n'écris pas de logiciels de classe affaires dans Scratch, donc, comme vous, j'ai la même attente qu'il est temps pour les langages de programmation matures de gérer automatiquement le problème synchrone / asynchrone.

Mikey Wetzel
la source
1

Le problème que vous décrivez est double.

  • Le programme que vous écrivez doit se comporter de manière asynchrone dans son ensemble lorsqu'il est vu de l'extérieur .
  • Il ne devrait pas être visible sur le site de l'appel, qu'un appel de fonction abandonne potentiellement le contrôle ou non.

Il y a deux façons d'y parvenir, mais elles se résument essentiellement à

  1. avoir plusieurs threads (à un certain niveau d'abstraction)
  2. ayant plusieurs types de fonctions au niveau du langage, qui sont tous appelés comme ça foo(4, 7, bar, quux).

Pour (1), je suis en train de regrouper forking et exécuter plusieurs processus, engendrant plusieurs threads du noyau et des implémentations de threads verts qui planifient les threads de niveau d'exécution du langage sur les threads du noyau. Du point de vue du problème, ce sont les mêmes. Dans ce monde, aucune fonction n'abandonne ou ne perd le contrôle du point de vue de son fil . Le thread lui-même n'a parfois pas de contrôle et parfois ne fonctionne pas, mais vous n'abandonnez pas le contrôle de votre propre thread dans ce monde. Un système correspondant à ce modèle peut ou non avoir la capacité de générer de nouveaux threads ou de se joindre à des threads existants. Un système correspondant à ce modèle peut ou non avoir la capacité de dupliquer un thread comme Unix fork.

(2) est intéressant. Pour que justice soit faite, nous devons parler de formulaires d'introduction et d'élimination.

Je vais montrer pourquoi l'implicite awaitne peut pas être ajouté à une langue comme Javascript d'une manière rétrocompatible. L'idée de base est qu'en exposant les promesses à l'utilisateur et en distinguant les contextes synchrone et asynchrone, Javascript a divulgué un détail d'implémentation qui empêche la gestion uniforme des fonctions synchrones et asynchrones. Il y a aussi le fait que vous ne pouvez pas faire de awaitpromesse en dehors d'un corps de fonction asynchrone. Ces choix de conception sont incompatibles avec "rendre l'asynchronisme invisible pour l'appelant".

Vous pouvez introduire une fonction synchrone à l'aide d'un lambda et l'éliminer avec un appel de fonction.

Introduction de la fonction synchrone:

((x) => {return x + x;})

Élimination de la fonction synchrone:

f(4)

((x) => {return x + x;})(4)

Vous pouvez comparer cela avec l'introduction et l'élimination de la fonction asynchrone.

Introduction à la fonction asynchrone

(async (x) => {return x + x;})

Élimination de la fonction asynchrone (remarque: uniquement valable à l'intérieur d'une asyncfonction)

await (async (x) => {return x + x;})(4)

Le problème fondamental ici est qu'une fonction asynchrone est également une fonction synchrone produisant un objet de promesse .

Voici un exemple d'appel synchrone d'une fonction asynchrone dans le repl node.js.

> (async (x) => {return x + x;})(4)
Promise { 8 }

Vous pouvez hypothétiquement avoir un langage, même typé dynamiquement, où la différence entre les appels de fonction asynchrones et synchrones n'est pas visible sur le site d'appel et n'est peut-être pas visible sur le site de définition.

Prendre un langage comme celui-ci et le réduire à Javascript est possible, il vous suffirait de rendre efficacement toutes les fonctions asynchrones.

Gregory Nisbet
la source
1

Avec les goroutines de langue Go et le temps d'exécution de la langue Go, vous pouvez écrire tout le code comme s'il s'agissait d'une synchrone. Si une opération se bloque dans un goroutine, l'exécution se poursuit dans d'autres goroutines. Et avec les canaux, vous pouvez communiquer facilement entre les goroutins. C'est souvent plus facile que les rappels comme en Javascript ou async / wait dans d'autres langues. Voir https://tour.golang.org/concurrency/1 pour quelques exemples et une explication.

De plus, je n'ai aucune expérience personnelle avec cela, mais j'entends qu'Erlang a des installations similaires.

Donc, oui, il existe des langages de programmation comme Go et Erlang, qui résolvent le problème synchrone / asynchrone, mais malheureusement ils ne sont pas encore très populaires. À mesure que ces langues gagnent en popularité, les installations qu'elles fournissent seront peut-être également implémentées dans d'autres langues.


la source
Je n'ai presque jamais utilisé la langue Go, mais il semble que vous le déclariez explicitement go ..., donc cela ressemble à await ...non?
Cinn
1
@Cinn En fait, non. Vous pouvez placer n'importe quel appel en tant que goroutine sur sa propre fibre / fil vert avec go. Et à peu près tout appel qui pourrait bloquer est effectué de manière asynchrone par le runtime, qui passe simplement à un autre goroutine entre-temps (multitâche coopératif). Vous attendez en attendant un message.
Déduplicateur
2
Bien que les Goroutines soient une sorte de concurrence, je ne les mettrais pas dans le même seau que async / wait: pas des coroutines coopératives mais automatiquement (et de manière préventive!) Des threads verts programmés. Mais cela ne rend pas non plus l'attente automatique: Go équivaut à awaitlire sur une chaîne <- ch.
amon
@amon Pour autant que je sache, les goroutines sont planifiées en coopération sur des threads natifs (normalement juste assez pour maximiser le véritable parallélisme matériel) par le runtime, et ceux-ci sont planifiés de manière préventive par le système d'exploitation.
Déduplicateur
L'OP a demandé "de pouvoir écrire du code asynchrone de manière synchrone". Comme vous l'avez mentionné, avec les goroutines et le go runtime, vous pouvez exactement le faire. Vous n'avez pas à vous soucier des détails du threading, il suffit d'écrire des lectures et des écritures bloquantes, comme si le code était synchrone, et vos autres goroutines, le cas échéant, continueront de fonctionner. Vous n'avez pas non plus à «attendre» ou à lire sur une chaîne pour bénéficier de cet avantage. Je pense donc que Go est un langage de programmation qui répond le mieux aux désirs du PO.
1

Il y a un aspect très important qui n'a pas encore été soulevé: la réentrance. Si vous avez un autre code (par exemple: boucle d'événements) qui s'exécute pendant l'appel asynchrone (et si vous n'en avez pas alors pourquoi avez-vous même besoin d'async?), Alors le code peut affecter l'état du programme. Vous ne pouvez pas masquer les appels asynchrones de l'appelant car l'appelant peut dépendre de certaines parties de l'état du programme pour ne pas être affectées pendant la durée de son appel de fonction. Exemple:

function foo( obj ) {
    obj.x = 2;
    bar();
    log( "obj.x equals 2: " + obj.x );
}

S'il bar()s'agit d'une fonction asynchrone, il peut être possible que le obj.xchange pendant son exécution. Ce serait plutôt inattendu sans aucune indication que la barre est asynchrone et que cet effet est possible. La seule alternative serait de suspecter chaque fonction / méthode possible d'être asynchrone et de récupérer et de revérifier tout état non local après chaque appel de fonction. Cela est sujet à des bogues subtils et peut même ne pas être possible du tout si une partie de l'état non local est récupérée via des fonctions. Pour cette raison, le programmeur doit savoir quelles fonctions ont le potentiel de modifier l'état du programme de manière inattendue:

async function foo( obj ) {
    obj.x = 2;
    await bar();
    log( "obj.x equals 2: " + obj.x );
}

Maintenant, il est clairement visible que la fonction bar()est une fonction asynchrone, et la bonne façon de la gérer est de revérifier la valeur attendue par la obj.xsuite et de traiter les modifications qui peuvent s'être produites.

Comme cela a déjà été noté par d'autres réponses, les langages fonctionnels purs comme Haskell peuvent échapper complètement à cet effet en évitant le besoin de tout état partagé / global. Je n'ai pas beaucoup d'expérience avec les langages fonctionnels, donc je suis probablement partisan de cela, mais je ne pense pas que l'absence d'état global soit un avantage lors de l'écriture d'applications plus volumineuses.

j_kubik
la source
0

Dans le cas de Javascript, que vous avez utilisé dans votre question, il y a un point important à prendre en compte: Javascript est monothread, et l'ordre d'exécution est garanti tant qu'il n'y a pas d'appels asynchrones.

Donc, si vous avez une séquence comme la vôtre:

const nbOfUsers = getNbOfUsers();

Vous avez la garantie que rien d'autre ne sera exécuté dans l'intervalle. Pas besoin de serrures ou quelque chose de similaire.

Cependant, si getNbOfUsersest asynchrone, alors:

const nbOfUsers = await getNbOfUsers();

signifie que pendant l' getNbOfUsersexécution, l'exécution donne et d'autres codes peuvent s'exécuter entre les deux. Cela peut à son tour nécessiter un certain verrouillage, selon ce que vous faites.

Donc, c'est une bonne idée de savoir quand un appel est asynchrone et quand il ne l'est pas, car dans certaines situations, vous devrez prendre des précautions supplémentaires dont vous n'auriez pas besoin si l'appel était synchrone.

jcaron
la source
Vous avez raison, mon deuxième code dans la question n'est pas valide comme si getNbOfUsers()renvoyait une promesse. Mais c'est exactement le point de ma question, pourquoi devons-nous l'écrire explicitement comme asynchrone, le compilateur pourrait le détecter et le gérer automatiquement d'une manière différente.
Cinn
@Cinn ce n'est pas mon point. Mon point est que le flux d'exécution peut atteindre d'autres parties de votre code pendant l'exécution de l'appel asynchrone, alors qu'il n'est pas possible pour un appel synchrone. Ce serait comme avoir plusieurs threads en cours d'exécution sans en être conscient. Cela peut se traduire par de gros problèmes (qui sont généralement difficiles à détecter et à reproduire).
jcaron
-4

Ceci est disponible en C ++ comme std::asyncdepuis C ++ 11.

La fonction modèle async exécute la fonction f de manière asynchrone (potentiellement dans un thread séparé qui peut faire partie d'un pool de threads) et renvoie un std :: future qui contiendra éventuellement le résultat de cet appel de fonction.

Et avec C ++ 20, les coroutines peuvent être utilisées:

Robert Andrzejuk
la source
5
Cela ne semble pas répondre à la question. Selon votre lien: "Qu'est-ce que le Coroutines TS nous donne? Trois nouveaux mots-clés de langue: co_await, co_yield et co_return" ... Mais la question est pourquoi avons-nous besoin d'un mot-clé await(ou co_awaitdans ce cas) en premier lieu?
Arturo Torres Sánchez