Logique d'entreprise asynchrone de nos jours

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:


  1. 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.
  2. 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).
  3. 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){ // ... }) .then([&](SomeObject::value &&val){ return Promise([&](Resolve&& resolve, Reject&&){ some_obj.funcCallback(resolve, val); }); }); } 

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){ // ... }) .then([ctx](SomeObject::Value &&val){ struct LocalContext2 { LocalContext2(std::shared_ptr<LocalContext> &&ctx, SomeObject::Value &&val) : ctx(ctx), val(val) {} std::shared_ptr<LocalContext> ctx; SomeObject::Value val; }; auto ctx2 = std::make_shared<LocalContext2>( std::move(ctx), std::forward<SomeObject::Value>(val) ); return Promise([ctx2](Resolve&& resolve, Reject&&){ ctx2->ctx->some_obj.funcCallback([ctx2, resolve](){ resolve(); }, val); }); }); } 

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) ( // ... } // Capture "async context" return Promise([async](Resolve&& resolve, Reject&&){ some_obj.funcCallback([async](){ resolve(); }, val); }); } 

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:


  1. Handle immediate(callack) nécessitant une pile d'appels propre.
  2. Rappel différé Traitement Handle deferred(delay, callback) .
  3. 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:


  1. add(func[, onerror]) - imitation de try-catch .
  2. success([args...]) - une indication explicite de réussite:
    • implicite par dĂ©faut
    • peut passer les rĂ©sultats Ă  l'Ă©tape suivante.
  3. 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.
  4. 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:


  1. loop( func, [, label] ) - étape avec un corps infiniment répétable.
  2. forEach( map|list, func [, label] ) - étape-itération de l'objet de collection.
  3. repeat( count, func [, label] ) - étape-itération nombre de fois spécifié.
  4. break( [label] ) est un analogue de l'interruption de boucle traditionnelle.
  5. 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) { // infinite loop asi.breakLoop(); }); asi.repeat(10, [](IAsyncSteps& asi, size_t i) { // range loop from i=0 till i=9 (inclusive) asi.continueLoop(); }); asi.forEach( std::vector<int>{1, 2, 3}, [](IAsyncSteps& asi, size_t i, int v) { // Iteration of vector-like and list-like objects }); asi.forEach( std::list<futoin::string>{"1", "2", "3"}, [](IAsyncSteps& asi, size_t i, const futoin::string& v) { // Iteration of vector-like and list-like objects }); asi.forEach( std::map<futoin::string, futoin::string>(), [](IAsyncSteps& asi, const futoin::string& key, const futoin::string& v) { // Iteration of map-like objects }); std::map<std::string, futoin::string> non_const_map; asi.forEach( non_const_map, [](IAsyncSteps& asi, const std::string& key, futoin::string& v) { // Iteration of map-like objects, note the value reference type }); 

API pour l'intégration avec des événements externes:


  1. 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.
  2. 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.
  3. 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 (...) { // pass } } else { asi.success(); } }); asi.setCancel([=](IAsyncSteps& asi) { external_cancel(handle); }); }); asi.add( [](IAsyncSteps& asi) { // Raises Timeout error after specified period asi.setTimeout(std::chrono::seconds{10}); asi.loop([](IAsyncSteps& asi) { // infinite loop }); }, [](IAsyncSteps& asi, ErrorCode code) { if (code == futoin::errors::Timeout) { asi(); } }); 

Exemple ECMAScript:


 asi.add( (asi) => { asi.waitExternal(); // disable implicit success() some_obj.read( (err, data) => { if (!asi.state) { // ignore as AsyncSteps execution got canceled } else if (err) { try { asi.error( 'IOError', err ); } catch (_) { // ignore error thrown as there are no // AsyncSteps frames on stack. } } else { asi.success( data ); } } ); } ); 

API d'intégration Future / Promise:


  1. await(promise_future[, on_error]) - en attendant Future / Promise comme étape.
  2. 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:


  1. AsyncSteps(AsyncTool&) est un constructeur qui lie un thread d'exécution à une boucle d'événement spécifique.
  2. execute() - démarre le thread d'exécution.
  3. 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:


  1. newInstance() - vous permet de créer un nouveau thread d'exécution sans dépendance directe sur l'implémentation.
  2. sync(object, func, onerror) - la même chose, mais avec une synchronisation relative à un objet qui implémente l'interface correspondante.
  3. 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


  1. Mutex - restreint l'exécution simultanée de N threads avec une file d'attente dans Q , par défaut N=1, Q=unlimited .
  2. 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 .
  3. 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 .


:


  1. Boost.Fiber protected_fixedsize_stack .
  2. Boost.Fiber pooled_fixedsize_stack .
  3. FutoIn AsyncSteps .
  4. FutoIn AsyncSteps ( FUTOIN_USE_MEMPOOL=false ).
    • futoin::IMemPool .
  5. FutoIn NitroSteps<> — .
    • .

Boost.Fiber :


  1. 1 . .
  2. 30 . 1 . .
    • 30 . mmap()/mprotect() boost::fiber::protected_fixedsize_stack .
    • .
  3. 30 . 10 . .
    • "" .

"" , .. , . . .


GCC 6.3.0. lang tcmalloc , .


GitHub GitLab .


1.


La technologie
Boost.Fiber protected4.8s208333.333Hz
Boost.Fiber pooled0.23s4347826.086Hz
FutoIn AsyncSteps0.21s4761904.761Hz
FutoIn AsyncSteps no mempool0.31s3225806.451Hz
FutoIn NitroSteps0.255s3921568.627Hz


— .


Boost.Fiber - , pooled_fixedsize_stack , AsyncSteps.


2.


La technologie
Boost.Fiber protected6.31s158478.605Hz
Boost.Fiber pooled1.558s641848.523Hz
FutoIn AsyncSteps1.13s884955.752Hz
FutoIn AsyncSteps no mempool1.353s739098.300Hz
FutoIn NitroSteps1.43s699300.699Hz


— .


, . , — .


3.


La technologie
Boost.Fiber protected5.096s1962323.390Hz
Boost.Fiber pooled5.077s1969667.126Hz
FutoIn AsyncSteps5.361s1865323.633Hz
FutoIn AsyncSteps no mempool8.288s1206563.706Hz
FutoIn NitroSteps3.68s2717391.304Hz


— .


, Boost.Fiber AsyncSteps, NitroSteps.


( RSS)


La technologie
Boost.Fiber protected124M
Boost.Fiber pooled505M
FutoIn AsyncSteps124M
FutoIn AsyncSteps no mempool84M
FutoIn NitroSteps115M


— .


, 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 .


TechSimpleLoop
Node.js v10
FutoIn AsyncSteps1342899.520Hz587.777Hz
async/await524983.234Hz630.863Hz
Node.js v8
FutoIn AsyncSteps682420.735Hz588.336Hz
async/await365050.395Hz400.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 .

Source: https://habr.com/ru/post/fr424311/


All Articles