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 ( nbOfUsers
vous 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.
await
saTask<T>
pour le convertir enT
async
/ à laawait
place, ce qui rend les parties asynchrones de l'exécution explicites.Réponses:
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.
la source
someValue ifItIsAFuture [self| self messageIWantToSend]
parce que c'est difficile à intégrer avec du code générique.par
peu près n'importe où dans du code Haskell pur et obtenir gratuitement le paralellisme.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.
la source
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.
la source
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>
. CelaFuture<Int>
a toutes les mêmes méthodes queInt
, 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 nouveauFuture<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 unFuture<Int>
, au lieu de cela automatiquementawait
le résultat, puis appellentself become: result.
, ce qui fera que l'objet en cours d'exécution (self
, c'est-à-dire leFuture<Int>
) devient littéralement l'result
objet, c'est-à-dire désormais la référence d'objet qui était auparavant unFuture<Int>
est maintenant unInt
partout, complètement transparent pour le client.Aucune fonctionnalité linguistique spéciale liée à l'asynchronie n'est requise.
la source
Future<T>
etT
partagent une interface commune et j'utiliser la fonctionnalité de cette interface. Doit-il enbecome
ré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.a + b
, les deux entiers, peu importe si a et b sont disponibles immédiatement ou plus tard, nous écrivons simplementa + b
(ce qui rend possible de le faireInt + Future<Int>
)Future<T>
etT
parce que de votre point de vue, il n'y a pasFuture<T>
, seulement aT
. 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.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:
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:
Trois mois plus tard, un joueur découvre qu'en se faisant tuer et en se déconnectant précisément lorsqu'il
attacker.addInventoryItems
est en cours d'exécution, ilvictim.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.
É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:
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:
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.
la source
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
Promise
s 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 ouCompletableFuture
s 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 enforkIO
abandonnant 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.
la source
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?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.
la source
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.
la source
Le problème que vous décrivez est double.
Il y a deux façons d'y parvenir, mais elles se résument essentiellement à
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
await
ne 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 deawait
promesse 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:
Élimination de la fonction synchrone:
Vous pouvez comparer cela avec l'introduction et l'élimination de la fonction asynchrone.
Introduction à la fonction asynchrone
Élimination de la fonction asynchrone (remarque: uniquement valable à l'intérieur d'une
async
fonction)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.
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.
la source
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
go ...
, donc cela ressemble àawait ...
non?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.await
lire sur une chaîne<- ch
.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:
S'il
bar()
s'agit d'une fonction asynchrone, il peut être possible que leobj.x
change 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: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 laobj.x
suite 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.
la source
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:
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
getNbOfUsers
est asynchrone, alors:signifie que pendant l'
getNbOfUsers
exé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.
la source
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.Ceci est disponible en C ++ comme
std::async
depuis C ++ 11.Et avec C ++ 20, les coroutines peuvent être utilisées:
la source
await
(ouco_await
dans ce cas) en premier lieu?