En bref:
- La preuve est déjà implémentée en C ++ , JS et PHP , adaptée à Java .
- Plus rapide que Coroutine et Promise, plus de fonctionnalités.
- Il ne nécessite pas de pile logicielle distincte.
- Se lie d'amitié avec tous les outils de sécurité et de débogage.
- Il fonctionne sur n'importe quelle architecture et ne nécessite pas de drapeaux de compilation spéciaux.
Regarder en arrière
À l'aube de l'ordinateur, il y avait un seul flux de contrôle avec blocage sur entrée-sortie. Puis des interruptions de fer y ont été ajoutées. Vous pouvez désormais utiliser efficacement des appareils lents et imprévisibles.
Avec la croissance des capacités de fer et sa faible disponibilité, il est devenu nécessaire d'effectuer plusieurs tâches simultanément, ce qui a fourni un support matériel. Il y avait donc des processus isolés avec des interruptions abstraites du fer sous forme de signaux.
L'étape évolutive suivante a été le multithreading, qui a été mis en œuvre sur la base des mêmes processus, mais avec un accès partagé à la mémoire et à d'autres ressources. Cette approche a ses limites et ses frais généraux importants pour passer à un système d'exploitation sécurisé.
Pour la communication entre processus et même différentes machines, l'abstraction Promise / Future a été proposée il y a plus de 40 ans.
Les interfaces utilisateur et le problème maintenant ridicule du client 10K ont conduit à l'apogée des approches Event Loop, Reactor et Proactor, qui sont plus orientées événements qu'une logique métier claire et cohérente.
Enfin, nous sommes arrivés à la coroutine moderne (coroutine), qui est essentiellement une émulation de flux au-dessus des abstractions décrites ci-dessus avec les limitations techniques correspondantes et le transfert déterministe de contrôle.
Pour transmettre des événements, des résultats et des exceptions, ils sont tous revenus au même concept de promesse / avenir. Certains bureaux ont décidé de nommer un peu différemment "Tâche".
En fin de compte, ils ont tout caché dans un beau paquet async/await
, qui nécessite le support du compilateur ou du traducteur selon la technologie.
Problèmes avec les situations de logique métier asynchrones actuelles
Considérez uniquement les coroutines et Promise, décorées d’ async/await
l'existence de problèmes dans les anciennes approches confirme le processus d'évolution lui-même.
Ces deux termes ne sont pas identiques. Par exemple, dans ECMAScript, il n'y a pas de coroutines, mais il existe un soulagement syntaxique pour l'utilisation de Promise
, qui à son tour organise uniquement le travail avec l'enfer de rappel. En fait, les moteurs de script comme V8 vont plus loin et font des optimisations spéciales pour les fonctions et les appels async/await
purs.
Les experts co_async/co_await
qui n'est pas tombé dans C ++ 17 ici sur la ressource , mais la pression des coroutines géantes du logiciel peut apparaître dans la norme exactement sous leur forme. En attendant, la solution traditionnellement reconnue est Boost.Context , Boost.Fiber et Boost.Coroutine2 .
En Java, il n'y a toujours pas d' async/await
au niveau du langage, mais il existe des solutions comme EA Async , qui, comme Boost.Context, doivent être personnalisées pour chaque version de la JVM et de l'octet de code.
Go a ses propres coroutines, mais si vous regardez attentivement les articles et les rapports de bogues des projets ouverts, il s'avère qu'ici tout n'est pas si fluide. Peut-être que perdre l'interface coroutine en tant qu'entité gérée n'est pas une bonne idée.
Opinion de l'auteur: les coroutines en métal nu sont dangereuses
Personnellement, l'auteur a peu contre les coroutines dans les langages dynamiques, mais il se méfie extrêmement de tout flirt avec la pile au niveau du code machine.
Quelques points:
- Pile requise:
- la pile sur le tas présente un certain nombre d'inconvénients: problèmes de détermination rapide du débordement, dommages causés par les voisins et autres problèmes de fiabilité / sécurité,
- une pile sécurisée nécessite au moins une page de mémoire physique, une page conditionnelle et une surcharge supplémentaire pour chaque appel aux fonctions
async
: 4 + Ko (minimum) + augmentation des limites du système, - en fin de compte, il se peut qu'une partie importante de la mémoire allouée aux piles ne soit pas utilisée pendant le temps d'arrêt de la coroutine.
- Il est nécessaire de mettre en œuvre une logique complexe pour enregistrer, restaurer et supprimer l'état de la coroutine:
- pour chaque cas d'architecture de processeur (même les modèles) et d'interface binaire (ABI): exemple ,
- des fonctionnalités d'architecture nouvelles ou optionnelles introduisent des problèmes potentiellement latents (par exemple, Intel TSX, co-processeurs ARM ou MIPS),
- d'autres problèmes potentiels dus à la documentation fermée des systèmes propriétaires (la documentation Boost s'y réfère).
- Problèmes potentiels avec les outils d'analyse dynamique et avec la sécurité en général:
- par exemple, l'intégration avec Valgrind est requise en raison des mêmes piles de sauts,
- il est difficile de parler pour les antivirus, mais probablement ils n'aiment pas vraiment ça sur l'exemple des problèmes avec la JVM dans le passé,
- Je suis sûr que de nouveaux types d'attaques apparaîtront et que les vulnérabilités associées à la mise en œuvre des coroutines seront révélées.
Opinion de l'auteur: générateurs et yield
mal fondamental
Ce thème apparemment tiers est directement lié au concept de coroutines et à la propriété "continue".
En bref, un itérateur complet doit exister pour toute collection. Pourquoi créer un problème d'itérateur-générateur recadré n'est pas clair. Par exemple, un cas avec range()
en Python est plus une démonstration exclusive qu'une excuse pour une complication technique.
Si le cas est un générateur infini, alors la logique de sa mise en œuvre est élémentaire. Pourquoi créer des difficultés techniques supplémentaires afin de pousser un cycle continu sans fin.
La seule justification raisonnable qui a émergé plus tard que les partisans des coroutines donnent est toutes sortes d'analyseurs de flux avec un contrôle inversé. En fait, il s'agit d'un cas spécialisé étroit pour la résolution de problèmes uniques au niveau de la bibliothèque, et non de la logique métier des applications. En même temps, il existe une solution élégante, simple et plus descriptive à travers les machines à états finis. Le domaine de ces problèmes techniques est beaucoup plus petit que le domaine de la logique commerciale courante.
En fait, le problème à résoudre est obtenu d'un doigt et nécessite des efforts relativement sérieux pour la mise en œuvre initiale et le support à long terme. À tel point que certains projets peuvent introduire une interdiction de l'utilisation des coroutines au niveau du code machine à l'instar de l'interdiction de goto
ou de l'utilisation de l'allocation dynamique de mémoire dans des industries individuelles.
Opinion des auteurs: Le modèle async/await
Promise d'ECMAScript est plus fiable, mais nécessite une adaptation
Contrairement aux coroutines continues, dans ce modèle, les morceaux de code sont secrètement divisés en blocs non interruptibles conçus comme des fonctions anonymes. En C ++, cela ne convient pas entièrement en raison des particularités de la gestion de la mémoire, un exemple:
struct SomeObject { using Value = std::vector<int>; Promise funcPromise() { return Promise.resolved(value_); } void funcCallback(std::function<void()> &&cb, const Value& val) { somehow_call_later(cb); } Value value_; }; Promise example() { SomeObject some_obj; return some_obj.funcPromise() .catch([](const std::exception &e){
Premièrement, some_obj
sera détruit en quittant example()
et avant d'appeler des fonctions lambda.
Deuxièmement, les fonctions lambda avec des variables ou des références de capture sont des objets et ajoutent secrètement un copier / déplacer, ce qui peut affecter négativement les performances avec un grand nombre de captures et la nécessité d'allouer de la mémoire sur le tas pendant l'effacement de type dans la std::function
habituelle.
Troisièmement, l'interface Promise
elle-même a été conçue sur le concept de «promesse» du résultat, plutôt que sur l'exécution cohérente de la logique métier.
Une solution schématique NON optimale pourrait ressembler à ceci:
Promise example() { struct LocalContext { SomeObject some_obj; }; auto ctx = std::make_shared<LocalContext>(); return some_obj.funcPromise() .catch([](const std::exception &e){
Remarque: std::move
au lieu de std::shared_ptr
ne convient pas en raison de l'impossibilité de transférer vers plusieurs lambdas à la fois et de la croissance de leur taille.
Avec l'ajout de async/await
les horreurs asynchrones se présentent dans un état digestible:
async void example() { SomeObject some_obj; try { SomeObject::Value val = await some_obj.func(); } catch (const std::exception& e) (
Opinion de l'auteur: le planificateur coroutine est un buste
Certains critiques considèrent le manque d'un planificateur et l'utilisation "malhonnête" des ressources du processeur un problème. Un problème plus grave est peut-être la localisation des données et l'utilisation efficace du cache du processeur.
Sur le premier problème: la hiérarchisation au niveau des coroutines individuelles ressemble à un gros frais généraux. Au lieu de cela, ils peuvent être utilisés en commun pour une tâche unifiée spécifique. C'est ce que font les flux de trafic.
Cela est possible en créant des instances distinctes de boucle d'événement avec leurs propres threads «de fer» et en planifiant au niveau du système d'exploitation. La deuxième option consiste à synchroniser les coroutines avec une primitive relativement primitive (Mutex, Throttle) en termes de compétition et / ou de performance.
La programmation asynchrone ne rend pas les ressources du processeur caoutchouteuses et nécessite des restrictions absolument normales sur le nombre de tâches traitées simultanément et des limites sur le temps d'exécution total.
La protection contre le blocage long sur une coroutine nécessite les mêmes mesures que pour les rappels - pour éviter de bloquer les appels système et les longs cycles de traitement des données.
Le deuxième problème nécessite des recherches, mais au moins la coroutine s'empile et les détails de la mise en œuvre de Future / Promise violent déjà la localité des données. Il est possible d'essayer de poursuivre l'exécution de la même coroutine si l'avenir compte déjà . Un certain mécanisme est nécessaire pour calculer le temps d'exécution ou le nombre de telles continuations afin d'empêcher une coroutine de capturer tout le temps du processeur. Cela peut ne pas donner de résultat, ou donner un résultat très double, selon la taille du cache du processeur et le nombre de threads.
Il y a aussi un troisième point - de nombreuses implémentations de planificateurs de coroutine leur permettent d'être exécutées sur différents cœurs de processeur, ce qui au contraire ajoute des problèmes en raison de la synchronisation obligatoire lors de l'accès aux ressources partagées. Dans le cas d'un seul flux de boucle d'événements, une telle synchronisation n'est requise qu'au niveau logique, car Chaque bloc de rappel synchrone est garanti de fonctionner sans course avec les autres.
L'avis de l'auteur: tout va bien avec modération
La présence de threads dans les systèmes d'exploitation modernes ne nie pas l'utilisation de processus individuels. En outre, le traitement d'un grand nombre de clients dans la boucle d'événements n'annule pas l'utilisation de threads «de fer» isolés pour d'autres besoins.
Dans tous les cas, les coroutines et les différentes variantes de boucles d'événements compliquent le processus de débogage sans le soutien nécessaire dans les outils, et avec les variables locales sur la pile de coroutines, tout devient encore plus difficile - il n'y a pratiquement aucun moyen d'y accéder.
FutoIn AsyncSteps - une alternative aux coroutines
Nous prenons comme base le modèle de boucle d'événement déjà bien établi et l'organisation des schémas de rappel selon le type de promesse ECMAScript (JavaScript).
En termes de planification d'exécution, nous nous intéressons aux activités suivantes d'Event Loop:
Handle immediate(callack)
nécessitant une pile d'appels propre.- Rappel différé Traitement
Handle deferred(delay, callback)
. - Annuler le callback
handle.cancel()
.
Nous obtenons donc une interface appelée AsyncTool
, qui peut être implémentée de nombreuses façons, y compris en plus des développements éprouvés existants. Il n'a aucun lien direct avec l'écriture de la logique métier, nous n'entrerons donc pas dans les détails.
Arbre des étapes:
Dans le concept AsyncSteps, un arbre abstrait d'étapes synchrones est aligné et exécuté en approfondissant la séquence de création. Les étapes de chaque niveau plus profond sont définies dynamiquement à mesure qu'un tel passage est terminé.
Toutes les interactions ont lieu via une seule interface AsyncSteps
, qui, par convention, est transmise comme premier paramètre à chaque étape. Par convention, le nom du paramètre est asi
ou obsolète. Cette approche vous permet de rompre presque complètement le lien entre une implémentation spécifique et l'écriture de la logique métier dans les plugins et les bibliothèques.
Dans les implémentations canoniques, chaque étape reçoit sa propre instance d'un objet qui implémente AsyncSteps
, ce qui permet un suivi rapide des erreurs logiques lors de l'utilisation de l'interface.
Exemple abstrait:
asi.add( // Level 0 step 1 func( asi ){ print( "Level 0 func" ) asi.add( // Level 1 step 1 func( asi ){ print( "Level 1 func" ) asi.error( "MyError" ) }, onerror( asi, error ){ // Level 1 step 1 catch print( "Level 1 onerror: " + error ) asi.error( "NewError" ) } ) }, onerror( asi, error ){ // Level 0 step 1 catch print( "Level 0 onerror: " + error ) if ( error strequal "NewError" ) { asi.success( "Prm", 123, [1, 2, 3], true) } } ) asi.add( // Level 0 step 2 func( asi, str_param, int_param, array_param ){ print( "Level 0 func2: " + param ) } )
Résultat d'exécution:
Level 0 func 1 Level 1 func 1 Level 1 onerror 1: MyError Level 0 onerror 1: NewError Level 0 func 2: Prm
En synchronisation, cela ressemblerait Ă ceci:
str_res, int_res, array_res, bool_res // undefined try { // Level 0 step 1 print( "Level 0 func 1" ) try { // Level 1 step 1 print( "Level 1 func 1" ) throw "MyError" } catch( error ){ // Level 1 step 1 catch print( "Level 1 onerror 1: " + error ) throw "NewError" } } catch( error ){ // Level 0 step 1 catch print( "Level 0 onerror 1: " + error ) if ( error strequal "NewError" ) { str_res = "Prm" int_res = 123 array_res = [1, 2, 3] bool_res = true } else { re-throw } } { // Level 0 step 2 print( "Level 0 func 2: " + str_res ) }
Le mimétisme maximal du code synchrone traditionnel est immédiatement visible, ce qui devrait aider à la lisibilité.
Du point de vue de la logique métier, un ensemble important d'exigences croît avec le temps, mais nous pouvons le diviser en parties faciles à comprendre. Décrit ci-dessous, le résultat d'un fonctionnement en pratique de quatre ans.
API Core Runtime:
add(func[, onerror])
- imitation de try-catch
.success([args...])
- une indication explicite de réussite:
- implicite par défaut
- peut passer les résultats à l'étape suivante.
error(code[, reason)
- interruption de l'exécution avec une erreur:
code
- a un type de chaîne pour mieux s'intégrer aux protocoles réseau dans l'architecture de microservice,reason
- une explication arbitraire pour une personne.
state()
- un analogue de Thread Local Storage. Clés associatives prédéfinies:
error_info
- explication de la dernière erreur pour une personne,last_exception
- pointeur vers l'objet de la dernière exception,async_stack
- une pile d'appels asynchrones dans la async_stack
où la technologie le permet,- le reste est défini par l'utilisateur.
L'exemple précédent est déjà avec du vrai code C ++ et quelques fonctionnalités supplémentaires:
#include <futoin/iasyncsteps.hpp> using namespace futoin; void some_api(IAsyncSteps& asi) { asi.add( [](IAsyncSteps& asi) { std::cout << "Level 0 func 1" << std::endl; asi.add( [](IAsyncSteps& asi) { std::cout << "Level 1 func 1" << std::endl; asi.error("MyError"); }, [](IAsyncSteps& asi, ErrorCode code) { std::cout << "Level 1 onerror 1: " << code << std::endl; asi.error("NewError", "Human-readable description"); } ); }, [](IAsyncSteps& asi, ErrorCode code) { std::cout << "Level 0 onerror 1: " << code << std::endl; if (code == "NewError") { // Human-readable error info assert(asi.state().error_info == "Human-readable description"); // Last exception thrown is also available in state std::exception_ptr e = asi.state().last_exception; // NOTE: smart conversion of "const char*" asi.success("Prm", 123, std::vector<int>({1, 2, 3}, true)); } } ); asi.add( [](IAsyncSteps& asi, const futoin::string& str_res, int int_res, std::vector<int>&& arr_res) { std::cout << "Level 0 func 2: " << str_res << std::endl; } ); }
API pour créer des boucles:
loop( func, [, label] )
- étape avec un corps infiniment répétable.forEach( map|list, func [, label] )
- étape-itération de l'objet de collection.repeat( count, func [, label] )
- étape-itération nombre de fois spécifié.break( [label] )
est un analogue de l'interruption de boucle traditionnelle.continue( [label] )
est un analogue de la continuation de boucle traditionnelle avec une nouvelle itération.
La spécification propose des noms alternatifs breakLoop
, continueLoop
et autres en cas de conflit avec des mots réservés.
Exemple C ++:
asi.loop([](IAsyncSteps& asi) {
API pour l'intégration avec des événements externes:
setTimeout( timeout_ms )
- renvoie une erreur de Timeout
après un timeout si l'étape et sa sous-arborescence n'ont pas terminé leur exécution.setCancel( handler )
- définit le gestionnaire d'annulation, qui est appelé lorsque le thread est complètement annulé et lorsque la pile d'étapes asynchrones est développée pendant le traitement des erreurs.waitExternal()
- une simple attente pour un événement externe.
- Remarque: Il est sûr de l'utiliser uniquement dans les technologies avec un garbage collector.
Un appel Ă l'une de ces fonctions rend un appel explicite Ă success()
nécessaire.
Exemple C ++:
asi.add([](IAsyncSteps& asi) { auto handle = schedule_external_callback([&](bool err) { if (err) { try { asi.error("ExternalError"); } catch (...) {
Exemple ECMAScript:
asi.add( (asi) => { asi.waitExternal();
API d'intégration Future / Promise:
await(promise_future[, on_error])
- en attendant Future / Promise comme étape.promise()
- transforme l'ensemble du flux d'exécution en Future / Promise, utilisé au lieu de execute()
.
Exemple C ++:
[](IAsyncSteps& asi) { // Proper way to create new AsyncSteps instances // without hard dependency on implementation. auto new_steps = asi.newInstance(); new_steps->add([](IAsyncSteps& asi) {}); // Can be called outside of AsyncSteps event loop // new_steps.promise().wait(); // or // new_steps.promise<int>().get(); // Proper way to wait for standard std::future asi.await(new_steps->promise()); // Ensure instance lifetime asi.state()["some_obj"] = std::move(new_steps); };
API Business Logic Flow Control:
AsyncSteps(AsyncTool&)
est un constructeur qui lie un thread d'exécution à une boucle d'événement spécifique.execute()
- démarre le thread d'exécution.cancel()
- annule le thread d'exécution.
Une implémentation d'interface spécifique est déjà requise ici.
Exemple C ++:
#include <futoin/ri/asyncsteps.hpp> #include <futoin/ri/asynctool.hpp> void example() { futoin::ri::AsyncTool at; futoin::ri::AsyncSteps asi{at}; asi.loop([&](futoin::IAsyncSteps &asi){ // Some infinite loop logic }); asi.execute(); std::this_thread::sleep_for(std::chrono::seconds{10}); asi.cancel(); // called in d-tor by fact }
autres API:
newInstance()
- vous permet de créer un nouveau thread d'exécution sans dépendance directe sur l'implémentation.sync(object, func, onerror)
- la même chose, mais avec une synchronisation relative à un objet qui implémente l'interface correspondante.parallel([on_error])
- spécial add()
, dont les sous-étapes sont des flux AsyncSteps distincts:
- tous les threads ont un
state()
commun state()
, - le thread parent continue son exécution à la fin de tous les enfants
- une erreur non détectée dans un enfant annule immédiatement tous les autres threads enfants.
Exemples C ++:
#include <futoin/ri/mutex.hpp> using namespace futoin; ri::Mutex mtx_a; void sync_example(IAsyncSteps& asi) { asi.sync(mtx_a, [](IAsyncSteps& asi) { // synchronized section asi.add([](IAsyncSteps& asi) { // inner step in the section // This synchronization is NOOP for already // acquired Mutex. asi.sync(mtx_a, [](IAsyncSteps& asi) { }); }); }); } void parallel_example(IAsyncSteps& asi) { using OrderVector = std::vector<int>; asi.state("order", OrderVector{}); auto& p = asi.parallel([](IAsyncSteps& asi, ErrorCode) { // Overall error handler asi.success(); }); p.add([](IAsyncSteps& asi) { // regular flow asi.state<OrderVector>("order").push_back(1); asi.add([](IAsyncSteps& asi) { asi.state<OrderVector>("order").push_back(4); }); }); p.add([](IAsyncSteps& asi) { asi.state<OrderVector>("order").push_back(2); asi.add([](IAsyncSteps& asi) { asi.state<OrderVector>("order").push_back(5); asi.error("SomeError"); }); }); p.add([](IAsyncSteps& asi) { asi.state<OrderVector>("order").push_back(3); asi.add([](IAsyncSteps& asi) { asi.state<OrderVector>("order").push_back(6); }); }); asi.add([](IAsyncSteps& asi) { asi.state<OrderVector>("order"); // 1, 2, 3, 4, 5 }); };
Primitives standard pour la synchronisation
Mutex
- restreint l'exécution simultanée de N
threads avec une file d'attente dans Q
, par défaut N=1, Q=unlimited
.Throttle
- limite le nombre d'entrées N
dans la période P
avec une file d'attente dans Q
, par défaut N=1, P=1s, Q=0
.Limiter
est une combinaison de Mutex
et Throttle
, qui est généralement utilisée à l'entrée du traitement des requêtes externes et lors de l'appel de systèmes externes dans le but d'un fonctionnement stable sous charge.
En cas de DefenseRejected
limites de file d'attente, une erreur DefenseRejected
est DefenseRejected
, dont la signification ressort clairement de la description du Limiter
.
Avantages clés
Le concept d'AsyncSteps n'était pas une fin en soi, mais est né du besoin d'une exécution asynchrone plus contrôlée des programmes en termes de délai, d'annulation et de connectivité globale des rappels individuels. Aucune des solutions universelles à l'époque et n'offre désormais les mêmes fonctionnalités. Par conséquent:
FTN12 — .
setCancel()
— . , . RAII atexit()
.
cancel()
— , . SIGTERM
pthread_cancel()
, .
setTimeout()
— . , "Timeout".
— FutoIn AsyncSteps .
— ABI , . Embedded MMU.
Intel Xeon E3-1245v2/DDR1333 Debian Stretch .
:
- Boost.Fiber
protected_fixedsize_stack
. - Boost.Fiber
pooled_fixedsize_stack
. - FutoIn AsyncSteps .
- FutoIn AsyncSteps (
FUTOIN_USE_MEMPOOL=false
).
- FutoIn NitroSteps<> — .
Boost.Fiber :
- 1 . .
- 30 . 1 . .
- 30 .
mmap()/mprotect()
boost::fiber::protected_fixedsize_stack
. - .
- 30 . 10 . .
"" , .. , . . .
GCC 6.3.0. lang tcmalloc , .
GitHub GitLab .
1.
La technologie | | |
---|
Boost.Fiber protected | 4.8s | 208333.333Hz |
Boost.Fiber pooled | 0.23s | 4347826.086Hz |
FutoIn AsyncSteps | 0.21s | 4761904.761Hz |
FutoIn AsyncSteps no mempool | 0.31s | 3225806.451Hz |
FutoIn NitroSteps | 0.255s | 3921568.627Hz |
— .
Boost.Fiber - , pooled_fixedsize_stack
, AsyncSteps.
2.
La technologie | | |
---|
Boost.Fiber protected | 6.31s | 158478.605Hz |
Boost.Fiber pooled | 1.558s | 641848.523Hz |
FutoIn AsyncSteps | 1.13s | 884955.752Hz |
FutoIn AsyncSteps no mempool | 1.353s | 739098.300Hz |
FutoIn NitroSteps | 1.43s | 699300.699Hz |
— .
, . , — .
3.
La technologie | | |
---|
Boost.Fiber protected | 5.096s | 1962323.390Hz |
Boost.Fiber pooled | 5.077s | 1969667.126Hz |
FutoIn AsyncSteps | 5.361s | 1865323.633Hz |
FutoIn AsyncSteps no mempool | 8.288s | 1206563.706Hz |
FutoIn NitroSteps | 3.68s | 2717391.304Hz |
— .
, Boost.Fiber AsyncSteps, NitroSteps.
La technologie | |
---|
Boost.Fiber protected | 124M |
Boost.Fiber pooled | 505M |
FutoIn AsyncSteps | 124M |
FutoIn AsyncSteps no mempool | 84M |
FutoIn NitroSteps | 115M |
— .
, Boost.Fiber .
: Node.js
- Promise
: + 10 . . 10 . JIT NODE_ENV=production
, @futoin/optihelp
.
GitHub GitLab . Node.js v8.12.0 v10.11.0, FutoIn CID .
Tech | Simple | Loop |
---|
Node.js v10 | | |
FutoIn AsyncSteps | 1342899.520Hz | 587.777Hz |
async/await | 524983.234Hz | 630.863Hz |
Node.js v8 | | |
FutoIn AsyncSteps | 682420.735Hz | 588.336Hz |
async/await | 365050.395Hz | 400.575Hz |
— .
async/await
? , V8 Node.js v10 .
, Promise async/await
Node.js Event Loop. ( ), FutoIn AsyncSteps .
AsyncSteps Node.js Event Loop async/await
- Node.js v10.
, ++ — . , Node.js 10 .
Conclusions
C++, FutoIn AsyncSteps Boost.Fiber , Boost.Fiber mmap()/mprotect
.
, - , . .
FutoIn AsyncSteps JavaScript async/await
Node.js v10.
, -, . .
- "" . — API.
Conclusion
, FutoIn AsyncSteps , "" async/await
. , . Promise
ECMAScript, AsyncSteps "" .
. AsyncSteps NitroSteps .
, - .
Java/JVM — . .
, GitHub / GitLab .