Lógica comercial asincrónica en estos días

En resumen:


  • La prueba ya está implementada en C ++ , JS y PHP , adecuada para Java .
  • Más rápido que coroutine y Promise, más funciones.
  • No requiere una pila de software separada.
  • Se hace amigo de todas las herramientas de seguridad y depuración.
  • Funciona en cualquier arquitectura y no requiere indicadores especiales del compilador.





Mirar hacia atrás


En los albores de la computadora, había un flujo de control único con bloqueo en la entrada-salida. Luego se le añadieron interrupciones de hierro. Ahora puede usar efectivamente dispositivos lentos e impredecibles.


Con el crecimiento de las capacidades de hierro y su baja disponibilidad, se hizo necesario realizar varias tareas simultáneamente, lo que proporcionó soporte de hardware. Así que hubo procesos aislados con interrupciones extraídas del hierro en forma de señales.


La siguiente etapa evolutiva fue el subprocesamiento múltiple, que se implementó sobre la base de los mismos procesos, pero con acceso compartido a la memoria y otros recursos. Este enfoque tiene sus limitaciones y una sobrecarga significativa para cambiar a un sistema operativo seguro.


Para la comunicación entre procesos e incluso máquinas diferentes, la abstracción Promise / Future se propuso hace más de 40 años.


Las interfaces de usuario y el problema del cliente 10K actualmente ridículo han llevado al apogeo de los enfoques Event Loop, Reactor y Proactor, que están más orientados a eventos que una lógica comercial clara y consistente.


Finalmente, llegamos a la corutina moderna (corutina), que en esencia es una emulación de flujos sobre las abstracciones descritas anteriormente con las correspondientes limitaciones técnicas y la transferencia de control determinista.


Para transmitir eventos, resultados y excepciones, todos volvieron al mismo concepto de Promesa / Futuro. Algunas oficinas decidieron nombrar un poco diferente: "Tarea".


Al final, escondieron todo en un hermoso paquete async/await , que requiere soporte de compilador o traductor dependiendo de la tecnología.


Problemas con situaciones actuales de lógica de negocios asincrónicas


Considere solo corutinas y Promesa, decoradas con async/await , como La existencia de problemas en los enfoques más antiguos confirma el proceso de evolución en sí.


Estos dos términos no son idénticos. Por ejemplo, en ECMAScript no hay corutinas, pero existe un alivio sintáctico para usar Promise , que a su vez solo organiza el trabajo con el infierno de devolución de llamadas. De hecho, los motores de secuencias de comandos como V8 van más allá y hacen optimizaciones especiales para funciones y llamadas puras y async/await .


Los expertos co_async/co_await no se co_async/co_await en C ++ 17 aquí en el recurso , pero la presión de las corutinas gigantes de software puede aparecer en el estándar exactamente en su forma. Mientras tanto, la solución tradicionalmente reconocida es Boost.Context , Boost.Fiber y Boost.Coroutine2 .


En Java, todavía no hay async/await a nivel de lenguaje, pero hay soluciones como EA Async , que, como Boost.Context, deben personalizarse para cada versión de JVM y byte de código.


Go tiene sus propias rutinas, pero si observa cuidadosamente los artículos y los informes de errores de los proyectos abiertos, resulta que aquí no todo es tan sencillo. Quizás perder la interfaz de rutina como entidad administrada no es una buena idea.


Opinión del autor: las corutinas de metal desnudo son peligrosas


Personalmente, el autor tiene poco en contra de las rutinas en lenguajes dinámicos, pero es extremadamente cauteloso con cualquier coqueteo con la pila a nivel de código de máquina.


Algunos puntos:


  1. Pila requerida:
    • stack on the heap tiene una serie de desventajas: problemas de determinación oportuna de desbordamiento, daños por vecinos y otros problemas de confiabilidad / seguridad,
    • una pila segura requiere al menos una página de memoria física, una página condicional y una sobrecarga adicional para cada llamada a las funciones async : 4 + KB (mínimo) + mayores límites del sistema,
    • en última instancia, puede ser que una parte significativa de la memoria asignada para las pilas no se use durante el tiempo de inactividad de rutina.
  2. Es necesario implementar una lógica compleja para guardar, restaurar y eliminar el estado de la rutina:
    • para cada caso de arquitectura de procesador (incluso modelos) e interfaz binaria (ABI): ejemplo ,
    • Las características de arquitectura nuevas u opcionales introducen problemas potencialmente latentes (por ejemplo, Intel TSX, coprocesadores ARM o MIPS),
    • Otros posibles problemas debido a la documentación cerrada de los sistemas patentados (la documentación de Boost se refiere a esto).
  3. Posibles problemas con las herramientas de análisis dinámico y con la seguridad en general:
    • por ejemplo, se requiere la integración con Valgrind debido a las mismas pilas de saltos,
    • es difícil hablar por los antivirus, pero probablemente no les guste en el ejemplo de los problemas con la JVM en el pasado,
    • Estoy seguro de que aparecerán nuevos tipos de ataques y se revelarán las vulnerabilidades asociadas con la implementación de las rutinas.

Opinión del autor: generadores y yield mal fundamental


Este tema aparentemente de terceros está directamente relacionado con el concepto de corutinas y la propiedad "continuar".


En resumen, debe existir un iterador completo para cualquier colección. Por qué crear un problema de generador de iterador recortado no está claro. Por ejemplo, un caso con range() en Python es más un alarde exclusivo que una excusa para una complicación técnica.


Si el caso es un generador infinito, entonces la lógica de su implementación es elemental. ¿Por qué crear dificultades técnicas adicionales para impulsar un ciclo continuo sin fin?


La única justificación sensata que surgió más tarde que los defensores de las corutinas dan es todo tipo de analizadores de flujo con control invertido. De hecho, este es un caso especializado estrecho para resolver problemas individuales a nivel de biblioteca, no la lógica de negocios de las aplicaciones. Al mismo tiempo, existe una solución elegante, simple y más descriptiva a través de máquinas de estados finitos. El área de estos problemas técnicos es mucho más pequeña que el área de la lógica empresarial común.


De hecho, el problema a resolver se obtiene de un dedo y requiere esfuerzos relativamente serios para la implementación inicial y el soporte a largo plazo. Tanto es así que algunos proyectos pueden introducir una prohibición sobre el uso de las rutinas de nivel de código de máquina siguiendo el ejemplo de una prohibición de goto a goto o el uso de asignación de memoria dinámica en industrias individuales.


Opinión de los autores: el modelo Promise async/await ECMAScript es más confiable, pero requiere adaptación


A diferencia de las rutinas continuas, en este modelo las piezas de código se dividen secretamente en bloques no interrumpibles diseñados como funciones anónimas. En C ++, esto no es del todo adecuado debido a las peculiaridades de la administración de memoria, un ejemplo:


 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); }); }); } 

En primer lugar, some_obj se destruirá al salir de example() y antes de llamar a funciones lambda.


En segundo lugar, las funciones lambda con variables o referencias de captura son objetos y secretamente agregan copiar / mover, lo que puede afectar negativamente el rendimiento con una gran cantidad de capturas y la necesidad de asignar memoria en el montón durante el borrado de tipo en la std::function habitual.


En tercer lugar, la interfaz de Promise en sí se concibió sobre el concepto de "promesa" del resultado, en lugar de la ejecución coherente de la lógica empresarial.


Una solución esquemática NO óptima podría verse así:


 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); }); }); } 

Nota: std::move lugar de std::shared_ptr no std::shared_ptr adecuado debido a la incapacidad de transferir a varias lambdas a la vez y al crecimiento de su tamaño.


Con la adición de async/await horrores asincrónicos vienen en un estado digerible:


 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); }); } 

Opinión del autor: el planificador de rutina es un fracaso


Algunos críticos consideran que la falta de un planificador y el uso "deshonesto" de los recursos del procesador son un problema. Quizás un problema más grave es la localidad de datos y el uso eficiente de la memoria caché del procesador.


Sobre el primer problema: la priorización a nivel de corutinas individuales parece una gran sobrecarga. En cambio, se pueden operar en común para una tarea unificada específica. Esto es lo que hacen los flujos de tráfico.


Esto es posible mediante la creación de instancias separadas de Event Loop con sus propios hilos "de hierro" y la planificación a nivel del sistema operativo. La segunda opción es sincronizar las rutinas con una primitiva relativamente primitiva (Mutex, Throttle) en términos de competencia y / o rendimiento.


La programación asincrónica no hace que los recursos del procesador sean gomosos y requiere restricciones absolutamente normales en el número de tareas procesadas simultáneamente y límites en el tiempo total de ejecución.


La protección contra el bloqueo prolongado en una rutina requiere las mismas medidas que con las devoluciones de llamada, para evitar el bloqueo de las llamadas al sistema y los largos ciclos de procesamiento de datos.


El segundo problema requiere investigación, pero al menos la rutina se acumula y los detalles de la implementación del Futuro / Promesa ya violan la localidad de los datos. Existe la oportunidad de intentar continuar la ejecución de la misma rutina si Future ya importa. Se necesita un cierto mecanismo para calcular el tiempo de ejecución o el número de tales continuaciones para evitar que una rutina capture el tiempo completo del procesador. Esto puede no dar un resultado o dar un resultado doble, dependiendo del tamaño de la memoria caché del procesador y el número de subprocesos.


También hay un tercer punto: muchas implementaciones de planificadores de rutina les permiten ejecutarse en diferentes núcleos de procesador, lo que, por el contrario, agrega problemas debido a la sincronización obligatoria al acceder a recursos compartidos. En el caso de una sola secuencia de Event Loop, dicha sincronización solo se requiere a nivel lógico, ya que Se garantiza que cada bloque de devolución de llamada síncrono funcionará sin una carrera con los demás.


Opinión del autor: todo es bueno con moderación


La presencia de hilos en los sistemas operativos modernos no niega el uso de procesos individuales. Además, el procesamiento de una gran cantidad de clientes en Event Loop no niega el uso de hilos aislados de "hierro" para otras necesidades.


En cualquier caso, las rutinas y diversas variantes de Event Loops complican el proceso de depuración sin el apoyo necesario en las herramientas, y con las variables locales en la pila de rutinas, todo se vuelve aún más difícil: prácticamente no hay forma de llegar a ellas.





FutoIn AsyncSteps: una alternativa a las corutinas


Tomamos como base el patrón ya establecido de Event Loop y la organización de esquemas de devolución de llamada de acuerdo con el tipo de promesa ECMAScript (JavaScript).


En términos de planificación de ejecución, estamos interesados ​​en las siguientes actividades de Event Loop:


  1. Handle immediate(callack) requiere una pila de llamadas limpia.
  2. Devolución de llamada Handle deferred(delay, callback) .
  3. Cancele la devolución de llamada handle.cancel() .

Entonces obtenemos una interfaz llamada AsyncTool , que se puede implementar de muchas maneras, incluso sobre los desarrollos probados existentes. No tiene relación directa con la escritura de la lógica empresarial, por lo que no entraremos en más detalles.


Árbol de pasos:


En el concepto AsyncSteps, un árbol abstracto de pasos sincrónicos se alinea y ejecuta al profundizar en la secuencia de creación. Los pasos de cada nivel más profundo se establecen dinámicamente a medida que se completa dicho pasaje.


Toda la interacción tiene lugar a través de una única interfaz AsyncSteps , que, por convención, se pasa como primer parámetro a cada paso. Por convención, el nombre del parámetro es asi o está en desuso as . Este enfoque le permite romper casi por completo la conexión entre una implementación específica y escribir lógica empresarial en complementos y bibliotecas.


En implementaciones canónicas, cada paso recibe su propia instancia de un objeto que implementa AsyncSteps , lo que permite el seguimiento oportuno de errores lógicos al usar la interfaz.


Ejemplo abstracto:


  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 ) } ) 

Resultado de ejecución:


  Level 0 func 1 Level 1 func 1 Level 1 onerror 1: MyError Level 0 onerror 1: NewError Level 0 func 2: Prm 

En sincronización, se vería así:


  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 ) } 

El mimetismo máximo del código síncrono tradicional es inmediatamente visible, lo que debería ayudar en la legibilidad.


Desde el punto de vista de la lógica empresarial, con el tiempo crece un gran conjunto de requisitos , pero podemos dividirlo en partes fáciles de entender. A continuación se describe el resultado de correr en la práctica durante cuatro años.


API de Core Runtime:


  1. add(func[, onerror]) - imitación de try-catch .
  2. success([args...]) : una indicación explícita de finalización exitosa:
    • implícito por defecto
    • puede pasar los resultados al siguiente paso.
  3. error(code[, reason) - interrupción de la ejecución con un error:
    • code : tiene un tipo de cadena para integrarse mejor con los protocolos de red en la arquitectura de microservicios,
    • reason : una explicación arbitraria para una persona.
  4. state() : un análogo de Thread Local Storage. Claves asociativas predefinidas:
    • error_info : explicación del último error para una persona,
    • last_exception : puntero al objeto de la última excepción,
    • async_stack : una pila de llamadas asíncronas en la async_stack que la tecnología lo permite,
    • el resto lo establece el usuario.

El ejemplo anterior ya es con código C ++ real y algunas características adicionales:


 #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 para crear bucles:


  1. loop( func, [, label] ) : paso con un cuerpo infinitamente repetible.
  2. forEach( map|list, func [, label] ) : iteración por pasos del objeto de colección.
  3. repeat( count, func [, label] ) : número de veces especificado de iteración por pasos.
  4. break( [label] ) es un análogo de la interrupción de bucle tradicional.
  5. continue( [label] ) es un análogo de la continuación del bucle tradicional con una nueva iteración.

La especificación ofrece nombres alternativos breakLoop , continueLoop y otros en caso de conflicto con palabras reservadas.


Ejemplo de 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 para integración con eventos externos:


  1. setTimeout( timeout_ms ) : arroja un error de tiempo de espera después de un tiempo de espera si el paso y su subárbol no han completado la ejecución.
  2. setCancel( handler ) : establece el controlador de cancelación, que se llama cuando el subproceso se cancela por completo y cuando la pila de pasos asincrónicos se expande durante el procesamiento de errores.
  3. waitExternal() : una simple espera para un evento externo.
    • Nota: Es seguro usarlo solo en tecnologías con un recolector de basura.

Una llamada a cualquiera de estas funciones hace necesaria una llamada explícita al success() .


Ejemplo de 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(); } }); 

Ejemplo de 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 de integración de futuro / promesa:


  1. await(promise_future[, on_error]) - esperando Future / Promise como un paso.
  2. promise() - convierte todo el flujo de ejecución en Future / Promise, usado en lugar de execute() .

Ejemplo de 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 de control de flujo de lógica de negocios:


  1. AsyncSteps(AsyncTool&) es un constructor que une un hilo de ejecución a un Event Loop específico.
  2. execute() : inicia el hilo de ejecución.
  3. cancel() - cancela el hilo de ejecución.

Ya se requiere una implementación de interfaz específica aquí.


Ejemplo de 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 } 

otras API:


  1. newInstance() : le permite crear un nuevo hilo de ejecución sin dependencia directa de la implementación.
  2. sync(object, func, onerror) : lo mismo, pero con sincronización relativa a un objeto que implementa la interfaz correspondiente.
  3. parallel([on_error]) - add() , cuyos pasos secundarios son secuencias AsyncSteps separadas:
    • todos los hilos tienen un state() común state() ,
    • el subproceso principal continúa la ejecución al finalizar todos los elementos secundarios
    • un error no detectado en cualquier hijo cancela inmediatamente todos los otros hilos hijos.

Ejemplos de 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 }); }; 

Primitivas estándar para sincronización


  1. Mutex : restringe la ejecución simultánea de N subprocesos con una cola en Q , de forma predeterminada N=1, Q=unlimited .
  2. Throttle : limita el número de entradas N en el período P con una cola en Q , por defecto N=1, P=1s, Q=0 .
  3. Limiter es una combinación de Mutex y Throttle , que generalmente se usa en la entrada de procesamiento de solicitudes externas y cuando se llaman a sistemas externos con el propósito de una operación estable bajo carga.

En caso de DefenseRejected límites de DefenseRejected cola, se DefenseRejected un error DefenseRejected , cuyo significado queda claro en la descripción del Limiter .


Beneficios clave


El concepto de AsyncSteps no era un fin en sí mismo, sino que nació de la necesidad de una ejecución asincrónica más controlada de programas en términos de límite de tiempo, cancelación y conectividad general de devoluciones de llamada individuales. Ninguna de las soluciones universales en ese momento y ahora ofrecen la misma funcionalidad. Por lo tanto:


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.


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.


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.


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)


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 .


Conclusiones


C++, FutoIn AsyncSteps Boost.Fiber , Boost.Fiber mmap()/mprotect .


, - , . .


FutoIn AsyncSteps JavaScript async/await Node.js v10.


, -, . .


- "" . — API.




Conclusión


, FutoIn AsyncSteps , "" async/await . , . Promise ECMAScript, AsyncSteps "" .


. AsyncSteps NitroSteps .


, - .


Java/JVM — . .


, GitHub / GitLab .

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


All Articles