Kurzum:
- Der Proof ist bereits in C ++ , JS und PHP implementiert, geeignet fĂŒr Java .
- Schneller als Coroutine und Promise, mehr Funktionen.
- Es ist kein separater Software-Stack erforderlich.
- Befreundet alle Sicherheits- und Debugging-Tools.
- Es funktioniert auf jeder Architektur und erfordert keine speziellen Compiler-Flags.
Schau zurĂŒck
Zu Beginn des Computers gab es einen einzigen Kontrollfluss mit Blockierung der Eingabe / Ausgabe. Dann wurden Eisenunterbrechungen hinzugefĂŒgt. Jetzt können Sie langsame und unvorhersehbare GerĂ€te effektiv verwenden.
Mit dem Wachstum der EisenkapazitĂ€ten und seiner geringen VerfĂŒgbarkeit wurde es notwendig, mehrere Aufgaben gleichzeitig auszufĂŒhren, was Hardware-UnterstĂŒtzung bot. Es gab also isolierte Prozesse mit Unterbrechungen, die in Form von Signalen vom Eisen abstrahiert wurden.
Die nĂ€chste Evolutionsstufe war Multithreading, das auf der Grundlage derselben Prozesse implementiert wurde, jedoch mit gemeinsamem Zugriff auf Speicher und andere Ressourcen. Dieser Ansatz hat seine Grenzen und einen erheblichen Aufwand fĂŒr den Wechsel zu einem sicheren Betriebssystem.
FĂŒr die Kommunikation zwischen Prozessen und sogar verschiedenen Maschinen wurde vor mehr als 40 Jahren die Abstraktion Promise / Future vorgeschlagen.
Die BenutzeroberflĂ€chen und das derzeit lĂ€cherliche 10K-Client-Problem haben zur BlĂŒtezeit der Event Loop-, Reactor- und Proactor-AnsĂ€tze gefĂŒhrt, die eher ereignisorientiert sind als eine klare, konsistente GeschĂ€ftslogik.
SchlieĂlich kamen wir zur modernen Coroutine (Coroutine), die im Wesentlichen eine Emulation von FlĂŒssen ĂŒber den oben beschriebenen Abstraktionen mit den entsprechenden technischen EinschrĂ€nkungen und dem deterministischen Kontrolltransfer ist.
Um Ereignisse, Ergebnisse und Ausnahmen zu vermitteln, kehrten alle zum gleichen Konzept von Versprechen / Zukunft zurĂŒck. Einige BĂŒros haben beschlossen, etwas anders zu benennen - "Aufgabe".
Am Ende versteckten sie alles in einem wunderschönen async/await
Paket, das je nach Technologie UnterstĂŒtzung durch Compiler oder Ăbersetzer erfordert.
Probleme mit aktuellen asynchronen GeschÀftslogiksituationen
Betrachten Sie nur Coroutinen und Promise, dekoriert mit async/await
, as Das Vorhandensein von Problemen in Àlteren AnsÀtzen bestÀtigt den Evolutionsprozess selbst.
Diese beiden Begriffe sind nicht identisch. Zum Beispiel gibt es in ECMAScript keine Coroutinen, aber es gibt eine syntaktische Erleichterung fĂŒr die Verwendung von Promise
, die wiederum nur die Arbeit mit der Callback-Hölle organisiert. In der Tat gehen Scripting-Engines wie V8 noch weiter und nehmen spezielle Optimierungen fĂŒr reine async/await
Funktionen und Aufrufe vor.
Experten co_async/co_await
, das hier auf der Ressource nicht in C ++ 17 co_async/co_await
, aber der Druck der Software-Riesen-Coroutinen kann im Standard genau in ihrer Form erscheinen. In der Zwischenzeit ist die traditionell anerkannte Lösung Boost.Context , Boost.Fiber und Boost.Coroutine2 .
In Java gibt es auf Sprachebene immer noch kein async/await
Warten , aber es gibt Lösungen wie EA Async , die wie Boost.Context fĂŒr jede Version der JVM und des Codebytes angepasst werden mĂŒssen.
Go hat seine eigenen Coroutinen, aber wenn man sich die Artikel und Fehlerberichte offener Projekte genau ansieht, stellt sich heraus, dass hier nicht alles so reibungslos ist. Vielleicht ist es keine gute Idee, die Coroutine-Schnittstelle als verwaltete EntitÀt zu verlieren.
Meinung des Autors: Bare-Metal-Coroutinen sind gefÀhrlich
Persönlich hat der Autor wenig gegen Coroutinen in dynamischen Sprachen, aber er ist Ă€uĂerst vorsichtig, wenn er auf der Ebene des Maschinencodes mit dem Stapel flirtet.
Ein paar Punkte:
- Stapel erforderlich:
- Der Stapel auf dem Haufen hat eine Reihe von Nachteilen: Probleme bei der rechtzeitigen Bestimmung des Ăberlaufs, SchĂ€den durch Nachbarn und andere ZuverlĂ€ssigkeits- / Sicherheitsprobleme,
- Ein geschĂŒtzter Stapel erfordert mindestens eine Seite physischen Speichers, eine bedingte Seite und zusĂ€tzlichen Overhead fĂŒr jeden Aufruf von
async
Funktionen: 4 + KB (Minimum) + erhöhte Systemlimits, - Letztendlich kann es sein, dass ein erheblicher Teil des fĂŒr die Stapel zugewiesenen Speichers wĂ€hrend der Ausfallzeit der Coroutine nicht verwendet wird.
- Es ist notwendig, eine komplexe Logik zum Speichern, Wiederherstellen und Löschen des Status von Coroutinen zu implementieren:
- fĂŒr jeden Fall von Prozessorarchitektur (auch Modelle) und binĂ€rer Schnittstelle (ABI): Beispiel :
- Neue oder optionale Architekturfunktionen fĂŒhren zu potenziell latenten Problemen (z. B. Intel TSX, ARM-Co-Prozessoren oder MIPS).
- andere potenzielle Probleme aufgrund der geschlossenen Dokumentation proprietÀrer Systeme (Boost-Dokumentation bezieht sich darauf).
- Mögliche Probleme mit dynamischen Analysetools und mit der Sicherheit im Allgemeinen:
- Zum Beispiel ist die Integration mit Valgrind aufgrund der gleichen Sprungstapel erforderlich.
- Es ist schwer, fĂŒr Virostatika zu sprechen, aber wahrscheinlich gefĂ€llt es ihnen am Beispiel von Problemen mit JVM in der Vergangenheit nicht wirklich.
- Ich bin sicher, dass neue Arten von Angriffen auftreten und Schwachstellen im Zusammenhang mit der Implementierung von Coroutinen aufgedeckt werden.
Meinung des Autors: Generatoren und yield
grundlegendes Ăbel
Dieses scheinbar von Dritten stammende Thema steht in direktem Zusammenhang mit dem Konzept der Coroutinen und der Eigenschaft "Fortfahren".
Kurz gesagt, fĂŒr jede Sammlung muss ein vollstĂ€ndiger Iterator vorhanden sein. Warum ein zugeschnittenes Iterator-Generator-Problem erstellt wird, ist nicht klar. Zum Beispiel ist ein Fall mit range()
in Python eher eine exklusive Demonstration als eine Entschuldigung fĂŒr technische Komplikationen.
Wenn der Fall ein unendlicher Generator ist, ist die Logik seiner Implementierung elementar. Warum zusÀtzliche technische Schwierigkeiten verursachen, um einen endlosen Zyklus fortzusetzen?
Die einzige vernĂŒnftige Rechtfertigung, die spĂ€ter von BefĂŒrwortern von Coroutinen gegeben wurde, sind alle Arten von Stream-Parsern mit umgekehrter Kontrolle. TatsĂ€chlich handelt es sich hierbei um einen engen Spezialfall zur Lösung einzelner Probleme auf Bibliotheksebene, nicht um die GeschĂ€ftslogik von Anwendungen. Gleichzeitig gibt es eine elegante, einfache und aussagekrĂ€ftigere Lösung durch endliche Zustandsmaschinen. Der Bereich dieser technischen Probleme ist viel kleiner als der Bereich der alltĂ€glichen GeschĂ€ftslogik.
TatsĂ€chlich wird das zu lösende Problem von einem Finger aus gelöst und erfordert relativ ernsthafte Anstrengungen fĂŒr die anfĂ€ngliche Implementierung und langfristige UnterstĂŒtzung. So sehr, dass einige Projekte möglicherweise ein Verbot der Verwendung von Coroutinen auf Maschinencode-Ebene einfĂŒhren, das dem Beispiel eines Verbots von goto
oder der Verwendung der dynamischen Speicherzuweisung in einzelnen Branchen folgt.
Meinung der Autoren: Das async/await
Promise-Modell von ECMAScript ist zuverlÀssiger, muss jedoch angepasst werden
Im Gegensatz zu fortlaufenden Coroutinen werden in diesem Modell die Codeteile heimlich in nicht unterbrechbare Blöcke unterteilt, die als anonyme Funktionen ausgelegt sind. In C ++ ist dies aufgrund der Besonderheiten der Speicherverwaltung nicht ganz geeignet. Ein Beispiel:
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){
Erstens wird some_obj
beim Beenden von example()
und vor dem Aufrufen von Lambda-Funktionen zerstört.
Zweitens sind Lambda-Funktionen mit Erfassungsvariablen oder Referenzen Objekte und fĂŒgen heimlich Kopieren / Verschieben hinzu, was sich negativ auf die Leistung bei einer groĂen Anzahl von Erfassungen und die Notwendigkeit auswirken kann, wĂ€hrend der Typlöschung in der ĂŒblichen std::function
Speicher auf dem Heap zuzuweisen.
Drittens wurde die Promise
Schnittstelle selbst eher nach dem Konzept des "Versprechens" des Ergebnisses als nach der konsequenten AusfĂŒhrung der GeschĂ€ftslogik konzipiert.
Eine schematische, NICHT optimale Lösung könnte ungefĂ€hr so ââaussehen:
Promise example() { struct LocalContext { SomeObject some_obj; }; auto ctx = std::make_shared<LocalContext>(); return some_obj.funcPromise() .catch([](const std::exception &e){
Hinweis: std::move
anstelle von std::shared_ptr
nicht geeignet, da nicht mehrere Lambdas gleichzeitig ĂŒbertragen werden können und ihre GröĂe zunimmt.
Mit der HinzufĂŒgung von async/await
asynchrone Schrecken in einen verdaulichen Zustand:
async void example() { SomeObject some_obj; try { SomeObject::Value val = await some_obj.func(); } catch (const std::exception& e) (
Meinung des Autors: Coroutine Planer ist eine Pleite
Einige Kritiker bezeichnen das Fehlen eines Schedulers und den "unehrlichen" Einsatz von Prozessorressourcen als Problem. Ein möglicherweise schwerwiegenderes Problem ist die DatenlokalitÀt und die effiziente Nutzung des Prozessor-Cache.
Zum ersten Problem: Die Priorisierung auf der Ebene einzelner Coroutinen sieht nach einem groĂen Aufwand aus. Stattdessen können sie gemeinsam fĂŒr eine bestimmte einheitliche Aufgabe bearbeitet werden. Dies ist, was Verkehrsströme tun.
Dies ist möglich, indem separate Instanzen der Ereignisschleife mit eigenen "Eisen" -Threads erstellt und auf Betriebssystemebene geplant werden. Die zweite Möglichkeit besteht darin, Coroutinen mit einem relativ primitiven (Mutex, Throttle) Primitiv in Bezug auf Wettbewerb und / oder Leistung zu synchronisieren.
Die asynchrone Programmierung macht die Prozessorressourcen nicht gummiartig und erfordert absolut normale EinschrĂ€nkungen hinsichtlich der Anzahl gleichzeitig verarbeiteter Aufgaben und EinschrĂ€nkungen der GesamtausfĂŒhrungszeit.
Der Schutz gegen langes Blockieren auf einer Coroutine erfordert die gleichen MaĂnahmen wie bei RĂŒckrufen - um das Blockieren von Systemaufrufen und lange Datenverarbeitungszyklen zu vermeiden.
Das zweite Problem erfordert Nachforschungen, aber zumindest die Coroutine-Stapel selbst und die Details der Future / Promise-Implementierung verletzen bereits die LokalitĂ€t der Daten. Es besteht die Möglichkeit, die AusfĂŒhrung derselben Coroutine fortzusetzen, wenn Future bereits eine Rolle spielt. Ein bestimmter Mechanismus ist erforderlich, um die AusfĂŒhrungszeit oder die Anzahl solcher Fortsetzungen zu berechnen, um zu verhindern, dass eine Coroutine die gesamte Prozessorzeit erfasst. AbhĂ€ngig von der GröĂe des Prozessor-Cache und der Anzahl der Threads kann dies entweder kein oder nur ein sehr zweifaches Ergebnis liefern.
Es gibt noch einen dritten Punkt: Viele Implementierungen von Coroutine-Schedulern ermöglichen die AusfĂŒhrung auf verschiedenen Prozessorkernen, was im Gegenteil zu Problemen aufgrund der obligatorischen Synchronisierung beim Zugriff auf gemeinsam genutzte Ressourcen fĂŒhrt. Im Fall eines einzelnen Ereignisschleifenstroms ist eine solche Synchronisation nur auf logischer Ebene erforderlich, da Jeder synchrone RĂŒckrufblock funktioniert garantiert ohne Rennen mit anderen.
Meinung des Autors: Alles ist gut in MaĂen
Das Vorhandensein von Threads in modernen Betriebssystemen negiert nicht die Verwendung einzelner Prozesse. Durch die Verarbeitung einer groĂen Anzahl von Clients in der Ereignisschleife wird die Verwendung isolierter "Eisen" -Threads fĂŒr andere Anforderungen nicht negiert.
In jedem Fall erschweren Coroutinen und verschiedene Varianten von Ereignisschleifen den Debugging-Prozess ohne die erforderliche UnterstĂŒtzung in den Tools, und mit lokalen Variablen auf dem Coroutine-Stapel wird alles noch schwieriger - es gibt praktisch keine Möglichkeit, an sie heranzukommen.
FutoIn AsyncSteps - eine Alternative zu Coroutinen
Wir stĂŒtzen uns auf das bereits etablierte Event-Loop-Muster und die Organisation von RĂŒckrufschemata nach dem Promise-Typ ECMAScript (JavaScript).
In Bezug auf die AusfĂŒhrungsplanung interessieren uns folgende AktivitĂ€ten aus Event Loop:
Handle immediate(callack)
die einen sauberen Aufrufstapel erfordern.- ZurĂŒckgestellter RĂŒckruf
Handle deferred(delay, callback)
RĂŒckruf Handle deferred(delay, callback)
. handle.cancel()
das RĂŒckruf- handle.cancel()
.
So erhalten wir eine Schnittstelle namens AsyncTool
, die auf viele Arten implementiert werden kann, auch zusÀtzlich zu bestehenden bewÀhrten Entwicklungen. Er hat keine direkte Beziehung zum Schreiben von GeschÀftslogik, daher werden wir nicht auf weitere Details eingehen.
Baum der Schritte:
Im AsyncSteps-Konzept wird ein abstrakter Baum synchroner Schritte aufgereiht und ausgefĂŒhrt, indem tief in die Erstellungssequenz hineingegangen wird. Die Schritte jeder tieferen Ebene werden dynamisch festgelegt, wenn eine solche Passage abgeschlossen ist.
Die gesamte Interaktion erfolgt ĂŒber eine einzige AsyncSteps
Schnittstelle, die AsyncSteps
Konvention als erster Parameter an jeden Schritt ĂŒbergeben wird. Konventionell ist der Parametername asi
oder veraltet as
. Mit diesem Ansatz können Sie die Verbindung zwischen einer bestimmten Implementierung und dem Schreiben von GeschÀftslogik in Plugins und Bibliotheken fast vollstÀndig trennen.
In kanonischen Implementierungen erhÀlt jeder Schritt eine eigene Instanz eines Objekts, das AsyncSteps
implementiert, wodurch logische Fehler bei der Verwendung der Schnittstelle rechtzeitig verfolgt werden können.
Abstraktes Beispiel:
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 ) } )
AusfĂŒhrungsergebnis:
Level 0 func 1 Level 1 func 1 Level 1 onerror 1: MyError Level 0 onerror 1: NewError Level 0 func 2: Prm
Synchron wĂŒrde es so aussehen:
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 ) }
Die maximale Nachahmung des herkömmlichen synchronen Codes ist sofort sichtbar, was die Lesbarkeit verbessern soll.
Aus Sicht der GeschĂ€ftslogik wĂ€chst mit der Zeit ein groĂer Teil der Anforderungen , aber wir können ihn in leicht verstĂ€ndliche Teile unterteilen. Im Folgenden wird das Ergebnis eines vierjĂ€hrigen Laufens in der Praxis beschrieben.
Kernlaufzeit-APIs:
add(func[, onerror])
- Nachahmung des try-catch
.success([args...])
- ein expliziter Hinweis auf den erfolgreichen Abschluss:
- standardmĂ€Ăig impliziert
- kann die Ergebnisse an den nĂ€chsten Schritt ĂŒbergeben.
error(code[, reason)
- Unterbrechung der AusfĂŒhrung mit einem Fehler:
code
- hat einen Zeichenfolgentyp zur besseren Integration in Netzwerkprotokolle in der Microservice-Architektur.reason
- eine willkĂŒrliche ErklĂ€rung fĂŒr eine Person.
state()
- ein Analogon zu Thread Local Storage. Vordefinierte assoziative SchlĂŒssel:
error_info
- ErklĂ€rung des letzten Fehlers fĂŒr eine Person,last_exception
- Zeiger auf das Objekt der letzten Ausnahme,async_stack
- ein Stapel asynchroner Aufrufe, async_stack
die Technologie dies zulÀsst,- Der Rest wird vom Benutzer festgelegt.
Das vorherige Beispiel enthÀlt bereits echten C ++ - Code und einige zusÀtzliche Funktionen:
#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 zum Erstellen von Schleifen:
loop( func, [, label] )
- Schritt mit einem unendlich wiederholbaren Körper.forEach( map|list, func [, label] )
- Schritt-Iteration des Auflistungsobjekts.repeat( count, func [, label] )
- Schritt-Iteration angegeben.break( [label] )
ist ein Analogon zum herkömmlichen Loop-Interrupt.continue( [label] )
ist ein Analogon zur traditionellen Schleifenfortsetzung mit einer neuen Iteration.
Die Spezifikation bietet alternative Namen breakLoop
, continueLoop
und andere im Falle eines Konflikts mit reservierten Wörtern.
C ++ Beispiel:
asi.loop([](IAsyncSteps& asi) {
API zur Integration mit externen Ereignissen:
setTimeout( timeout_ms )
- setTimeout( timeout_ms )
nach einem Timeout einen Timeout
Fehler aus, wenn der Schritt und sein Teilbaum die AusfĂŒhrung nicht abgeschlossen haben.setCancel( handler )
- setCancel( handler )
, der aufgerufen wird, wenn der Thread vollstÀndig abgebrochen wird und wenn der Stapel asynchroner Schritte wÀhrend der Fehlerverarbeitung erweitert wird.waitExternal()
- ein einfaches Warten auf ein externes Ereignis.
- Hinweis: Die Verwendung ist nur in Technologien mit einem Garbage Collector sicher.
Ein Aufruf einer dieser Funktionen macht einen expliziten Aufruf von success()
erforderlich.
C ++ Beispiel:
asi.add([](IAsyncSteps& asi) { auto handle = schedule_external_callback([&](bool err) { if (err) { try { asi.error("ExternalError"); } catch (...) {
ECMAScript-Beispiel:
asi.add( (asi) => { asi.waitExternal();
Future / Promise Integration API:
await(promise_future[, on_error])
- Warten auf Zukunft / Versprechen als Schritt.promise()
- verwandelt den gesamten AusfĂŒhrungsfluss in Future / Promise, das anstelle von execute()
.
C ++ Beispiel:
[](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 zur Steuerung des GeschÀftslogikflusses:
AsyncSteps(AsyncTool&)
ist ein Konstruktor, der einen AusfĂŒhrungsthread an eine bestimmte Ereignisschleife bindet.execute()
- Startet den AusfĂŒhrungsthread.cancel()
- Bricht den AusfĂŒhrungsthread ab.
Hier ist bereits eine spezifische Schnittstellenimplementierung erforderlich.
C ++ Beispiel:
#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 }
andere APIs:
newInstance()
- Ermöglicht das Erstellen eines neuen AusfĂŒhrungsthreads ohne direkte AbhĂ€ngigkeit von der Implementierung.sync(object, func, onerror)
- das gleiche, jedoch mit Synchronisation relativ zu einem Objekt, das die entsprechende Schnittstelle implementiert.parallel([on_error])
- spezielles add()
, dessen Teilschritte separate AsyncSteps-Streams sind:
- Alle Threads haben den gemeinsamen
state()
. - Der ĂŒbergeordnete Thread setzt die AusfĂŒhrung nach Abschluss aller untergeordneten Threads fort
- Ein nicht erfasster Fehler in einem untergeordneten Element löscht sofort alle anderen untergeordneten Threads.
C ++ - Beispiele:
#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 }); };
Standardprimitive fĂŒr die Synchronisation
Mutex
- schrĂ€nkt die gleichzeitige AusfĂŒhrung von N
Threads mit einer Warteschlange in Q
, standardmĂ€Ăig N=1, Q=unlimited
.Throttle
- begrenzt die Anzahl der EingÀnge N
in Periode P
mit einer Warteschlange in Q
, standardmĂ€Ăig N=1, P=1s, Q=0
.Limiter
ist eine Kombination aus Mutex
und Throttle
, die normalerweise bei der Eingabe der Verarbeitung externer Anforderungen und beim Aufrufen externer Systeme zum Zwecke eines stabilen Betriebs unter Last verwendet wird.
Bei DefenseRejected
Warteschlangenlimits wird ein DefenseRejected
Fehler DefenseRejected
, dessen Bedeutung aus der Limiter
Beschreibung hervorgeht.
Hauptvorteile
Das Konzept von AsyncSteps war kein Selbstzweck, sondern entstand aus der Notwendigkeit einer kontrollierten asynchronen AusfĂŒhrung von Programmen hinsichtlich Zeitlimit, Abbruch und GesamtkonnektivitĂ€t einzelner RĂŒckrufe. Keine der universellen Lösungen zu dieser Zeit und bietet jetzt die gleiche FunktionalitĂ€t. Deshalb:
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.
| Zeit | |
---|
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.
| Zeit | |
---|
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.
| Zeit | |
---|
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.
| Die Erinnerung |
---|
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 .
Schlussfolgerungen
C++, FutoIn AsyncSteps Boost.Fiber , Boost.Fiber mmap()/mprotect
.
, - , . .
FutoIn AsyncSteps JavaScript async/await
Node.js v10.
, -, . .
- "" . â API.
Fazit
, FutoIn AsyncSteps , "" async/await
. , . Promise
ECMAScript, AsyncSteps "" .
. AsyncSteps NitroSteps .
, - .
Java/JVM â . .
, GitHub / GitLab .