Corrutinas (C++20)
Una corrutina es una función que puede suspender su ejecución para reanudarla más tarde. Las corrutinas no tienen pila: suspenden la ejecución regresando al punto de llamada y los datos requeridos para reanudar la ejecución se almacenan por separado de la pila. Esto admite que código secuencial se ejecute asincrónicamente (por ejemplo, para manejar E/S sin bloqueo sin funciones de devolución de llamada explícitas), y también admite algoritmos en secuencias infinitas ejecutadas de forma perezosa y otros usos.
Una función es una corrutina si su definición hace cualquiera de los siguientes:
- usa el operador co_await para suspender la ejecución hasta que se reanude
task<> tcp_eco_servidor() { char datos[1024]; while (true) { size_t n = co_await socket.leer_algo_asinc(buffer(datos)); co_await escribir_asinc(socket, buffer(datos, n)); } }
- usa la palabra clave co_yield para suspender la ejecución devolviendo un valor
generator<int> iota(int n = 0) { while(true) co_yield n++; }
- usa la palabra clave co_return para completar la ejecución devolviendo un valor
lazy<int> f() { co_return 7; }
Cada corrutina debe tener un tipo de retorno que satisfaga una serie de requisitos, que se indican a continuación.
Contenido |
[editar] Restricciones
Las corrutinas no pueden usar argumentos variádicos, instrucciones return simples, o marcadores de posición para los tipos de retorno (auto
o Concept
).
No pueden ser corrutinas las funciones constexpr, los constructores, los destructores, y la función main.
[editar] Ejecución
Cada corrutina está asociada con:
- El objeto promesa, manipulado desde el interior de la corrutina. La corrutina envía su resultado o excepción a través de este objeto.
- El identificador de corrutina, manipulado desde fuera de la corrutina. Es un identificador no propietario que se usa para reanudar la ejecución de la corrutina o para destruir el marco de la corrutina.
- El estado de la corrutina, que es un objeto interno, alojado en el montículo de memoria (a menos que la asignación esté optimizada), que contiene
- el objeto promesa;
- los parámetros (todos copiados por valor);
- alguna representación del punto de suspensión actual, para que la reanudación sepa dónde continuar y la destrucción sepa qué variables locales estaban en ámbito.
- variables locales y objetos temporales cuyo tiempo de vida abarca el punto de suspensión actual.
Cuando una corrutina inicia la ejecución, realiza lo siguiente:
- asigna memoria para el estado de la corrutina usando operator new (ver más abajo);
- copia todos los parámetros de función al estado de la corrutina: los parámetros por valor se copian o se mueven, los parámetros por referencia siguen siendo referencias (y, por lo tanto, pueden quedar pendientes si la corrutina se reanuda después de terminada el tiempo de vida del objeto referenciado).
#include <coroutine> #include <iostream> struct promesa; struct corrutina : std::coroutine_handle<promesa> { using promise_type = struct promesa; }; struct promesa { corrutina get_return_object() { return {corrutina::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; struct S { int i; corrutina f() { std::cout << i; co_return; } }; void mal1() { corrutina h = S{0}.f(); // S{0} destruido h.resume(); // la corrutina reanudada ejecuta std::cout << i, usa S::i después de liberada h.destroy(); } corrutina mal2() { S s{0}; return s.f(); // la corrutina devuelta no se puede reanudar sin comprometer el uso // después de la liberación } void mal3() { corrutina h = [i = 0]() -> corrutina { // una lambda que también es una corrutina std::cout << i; co_return; }(); // invocado inmediatamente // lambda destruido h.resume(); // usa (tipo lambda anónimo)::i después de liberado h.destroy(); } void bien() { coroutine h = [](int i) -> coroutine { // i es un parámetro de corrutina std::cout << i; co_return; }(0); // lambda destruida h.resume(); // sin problemas, i se copió en el marco de la corrutina como un // parámetro por valor h.destroy(); }
- llama al constructor del objeto promesa. Si el tipo promesa tiene un constructor que toma todos los parámetros de la corrutina, se llama a este, con copia posterior de argumentos de corrutina. De lo contrario se llama al constructor por defecto.
- llama a promesa.get_return_object() y mantiene el resultado en una variable local. El resultado de esa llamada se devolverá al llamante cuando la corrutina se suspenda por primera vez. Cualquier excepción lanzada hasta este paso (incluyéndolo) se propaga al llamante, no se coloca en la promesa.
- llama a promise.initial_suspend() y co_await su resultado. Los tipos típicos de Promesa devuelven suspend_always, para corrutinas iniciadas de manera perezosa, o suspend_never, para corrutinas iniciadas vorazmente.
- cuando co_await promesa.initial_suspend() reanuda, comienza a ejecutar el cuerpo de la corrutina.
Cuando una corrutina alcanza un punto de suspensión
- el objeto de retorno obtenido anteriormente se devuelve al llamante/reanudador, después de la conversión implícita al tipo de retorno de la corrutina, si es necesario.
Cuando una corrutina alcanza la instrucción co_return, realiza lo siguiente:
- llama a promesa.return_void() para
- co_return;
- co_return expr cuando
expr
tiene tipo void - llegar al final de una corrutina que devuelve void. En este caso, el comportamiento no está definido si el tipo Promesa no tiene una función miembro Promesa::return_void().
- o llama a promesa.return_value(expr) para co_return expr donde
expr
tiene tipo no void - destruye todas las variables con duración de almacenamiento automático en orden inverso a como fueron creadas.
- llama a promesa.final_suspend() y co_await el resultado.
Si la corrutina termina con una excepción sin atrapar, realiza lo siguiente:
- atrapa la excepción y llama a promise.unhandled_exception() desde dentro del bloque catch
- llama a promise.final_suspend() y co_await el resultado (por ejemplo, para reanudar una continuación o publicar un resultado). Reanudar desde este punto tiene un comportamiento no definido.
Cuando el estado de la corrutina se destruye porque terminó a través de co_return o una excepción no atrapada, o porque se destruyó a través de su identificador, hace lo siguiente:
- llama al destructor del objeto promesa.
- llama a los destructores de las copias de los parámetros de función.
- llama al operator delete para libarar la memoria usada por el estado de corrutina.
- transfiere de nuevo la ejecución al llamante/reanudador.
[editar] Asignación de memoria en el montículo
El estado de corrutina se asigna en el montículo de memoria mediante el operator new que no es un array.
Si el tipo Promesa define un reemplazo a nivel de clase, se usará, de lo contrario, se usará el operator new global.
Si el tipo Promesa define una forma de operator new de ubicación que toma parámetros adicionales, y coinciden con una lista de argumentos donde el primer argumento es el tamaño solicitado (de tipo std::size_t) y el resto son los argumentos de la función corrutina, esos argumentos se pasarán a operator new (esto permite el uso de convención de asignador principal para corrutinas).
La llamada a operator new se puede optimizar (incluso si se usa un asignador de memoria personalizado) si
- el tiempo de vida del estado de la corrutina está estrictamente anidado dentro el tiempo de vida del llamante, y
- y el tamaño del marco de la corrutina se conoce en el sitio de la llamada.
En ese caso, el estado de la corrutina está incrustado en el marco de pila del llamante (si el llamante es una función ordinaria) o en el estado de la corrutina (si el llamante es una corrutina).
Si la asignación falla, la corrutina lanza std::bad_alloc, a no ser que el tipo Promesa defina la función miembro Promesa::get_return_object_on_allocation_failure(). Si esta función miembro está definida, la asignación de memoria utiliza la forma nothrow de operator new y si se genera un error de asignación de memoria, la corrutina devuelve inmediatamente el objeto obtenido de Promesa::get_return_object_on_allocation_failure() al llamante.
[editar] Promesa
El tipo Promesa está determinado por el compilador a partir del tipo de retorno de la corrutina mediante std::coroutine_traits.
Formalmente, permite a R
y Args...
indicar el tipo de retorno y la lista de tipos de parámetro respectivamente, ClassT
y /*calificación-cv*/ (si hay) indica el tipo de clase al que pertenece la corrutina y su calificación-cv respectivamente si se define como una función miembro no estática, su tipo Promesa
está determinado por:
- std::coroutine_traits<R, Args...>::promise_type, si la corrutina no se define como una función miembro no estática,
- std::coroutine_traits<R, ClassT /*calificación-cv*/&, Args...>::promise_type, si la corrtuina se define como una función miembro no estática que no es una referencia calificada r-valor,
- std::coroutine_traits<R, ClassT /*calificación-cv*/&&, Args...>::promise_type, si la corrrutina se define como una función miembro no estática que es una referencia calificada r-valor.
Por ejemplo:
- Si la corrutina se define como task<float> foo(std::string x, bool flag);, entonces su tipo
Promesa
es std::coroutine_traits<task<float>, std::string, bool>::promise_type. - Si la corrutina se define como task<void> my_class::method1(int x) const;, su tipo
Promesa
es std::coroutine_traits<task<void>, const my_class&, int>::promise_type. - Si la corrutina se define como task<void> my_class::method1(int x) &&;, su tipo
Promesa
es std::coroutine_traits<task<void>, my_class&&, int>::promise_type.
[editar] co_await
El operador unario co_await suspende una corrutina y devuelve el control al llamante. Su operando es una expresión cuyo tipo debe definir operator co_await, o ser convertible a dicho tipo por medio de Promesa::await_transform de la corrutina actual.
co_await expr
|
|||||||||
Primero, expr se convierte en aguardable (awaitable), como sigue:
- si expr se produce por un punto de suspensión inicial, un punto de suspensión final o una expresión yield, el aguardable es expr, tal cual.
- de lo contrario, si el tipo Promesa de la corrutina actual tiene la función miembro await_transform, entonces el aguardable es promise.await_transform(expresión)
- de lo contrario, el aguardable es expr, tal cual.
Luego, se obtiene el objeto aguardador (awaiter), de la siguiente manera:
- si la resolución de sobrecarga para operator co_await obtiene una sola mejor sobrecarga, el aguardador es el resultado de esa llamada (awaitable.operator co_await() para la sobrecarga de miembros, operator co_await(static_cast<Awaitable&&>(awaitable)) para la sobrecarga de no miembros)
- de lo contrario, si la resolución de sobrecarga no encuentra ningún operator co_await, el aguardador es aguardable, tal cual
- de lo contrario, si es ambigua la resolución de sobrecarga, el programa está mal formado
Si la expresión anterior es un pr-valor, el objeto aguardador es un temporal materializado a partir de él. De lo contrario, si la expresión anterior es un gl-valor, el objeto aguardador es el objeto al que se refiere.
Después, se llama a awaiter.await_ready() (este es una abreviación para evitar el costo de la suspensión si se sabe que el resultado está listo o se puede completar sincrónicamente). Si su resultado, contextualmente convertido a booleano es false entonces
- La corrutina se suspende (su estado se rellena con variables locales y el punto de suspensión actual).
- Se llama a awaiter.await_suspend(handle), donde
handle
es el identificador de corrutina que representa la corrutina actual. Dentro de esta función, se puede ver el estado de la corrutina suspendida a través de este identificador, y es responsabilidad de esta función programarla para que se reanude en algún ejecutor, o para que se destruya (devolviendo recuentos falsos como programación)- si
await_suspend
devuelve void, se devuelve inmediatamente el control al llamante/reanudador de la corrutina actual (esta rutina sigue suspendida), si no - si
await_suspend
devuelve un booleano,
- el valor true devuelve el control al llamante/reanudador de la corrutina actual
- el valor false reanuda la corrutina actual.
- si
await_suspend
devuelve un identificador de corrutina para alguna otra corrutina, este identificador se reanuda (mediante una llamada a handle.resume()) (ten en cuenta que esto puede encadenarse para eventualmente hacer que la corrutina actual se reanude) - si
await_suspend
lanza una excepción, se atrapa la excepción, se reanuda la corrutina, y la excepción se relanza inmediatamente
- si
- Finalmente, se llama a awaiter.await_resume(), y su resultado es el resultado de toda la expresión co_await expr.
Si la corrutina se suspendió en la expresión co_await, y luego se reanuda, el punto de reanudación es inmediatamente anterior a la llamada a awaiter.await_resume().
Ten en cuenta que debido a que la corrutina está totalmente suspendida antes de entrar en awaiter.await_suspend(), esta función es libre de transferir el identificador de la corrutina entre hilos, sin necesidad de sincronización adicional. Por ejemplo, puede ponerlo dentro de una devolución de llamada, programada para ejecutarse en un grupo de hilos cuando se complete una operación E/S asíncrona. En este caso, dado que la corrutina actual puede haberse reanudado y, por lo tanto, ejecutado el destructor del objeto aguardador, al mismo tiempo que await_suspend()
continúa su ejecución en el hilo actual, await_suspend()
debería tratar a *this como destruido y acceder a él después de que el identificador se haya publicado en otros hilos.
[editar] Ejemplo
#include <coroutine> #include <iostream> #include <stdexcept> #include <thread> auto cambiar_a_nuevo_hilo(std::jthread& externo) { struct aguardable { std::jthread* p_externo; bool await_ready() { return false; } void await_suspend(std::coroutine_handle<> h) { std::jthread& externo = *p_externo; if (externo.joinable()) throw std::runtime_error("El parámetro de salida jthread no está vacío"); externo = std::jthread([h] { h.resume(); }); // Comportamiento no definido potencial: acceso a posible *this destruido // std::cout << "ID nuevo hilo: " << p_externo->get_id() << '\n'; std::cout << "ID nuevo hilo: " << externo.get_id() << '\n'; // esto es correcto } void await_resume() {} }; return aguardable{&externo}; } struct tarea { struct promise_type { tarea get_return_object() { return {}; } std::suspend_never initial_suspend() { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; }; tarea reanudacion_en_nuevo_hilo(std::jthread& externo) { std::cout << "Corrutina comienza en hilo: " << std::this_thread::get_id() << '\n'; co_await cambiar_a_nuevo_hilo(externo); // aguardador destruido aquí std::cout << "Corrutina reanudada en hilo: " << std::this_thread::get_id() << '\n'; } int main() { std::jthread externo; reanudacion_en_nuevo_hilo(externo); }
Posible salida:
Corrutina comienza en hilo: 139972277602112 ID nuevo hilo: 139972267284224 Corrutina reanudada en hilo: 139972267284224
Nota: el objeto aguardador es parte del estado de la corrutina (como un temporal cuyo tiempo de vida cruza un punto de suspensión) y se destruye antes de que termine la expresión co_await. Se puede usar para mantener el estado por operación como requieren algunas API de E/S asíncrona sin recurrir a asignaciones adicionales en el montículo de memoria.
La biblioteca estándar define dos aguardables triviales: std::suspend_always y std::suspend_never.
Esta sección está incompleta Razón: ejemplos |
[editar] co_yield
La expresión yield devuelve un valor al llamante y suspende la corrutina actual: es el bloque de construcción común de las funciones del generador reanudable
co_yield expr
|
|||||||||
co_yield lista-iniciación-entre-llaves
|
|||||||||
es equivalente a
co_await promise.yield_value(expresión)
El yield_value
de un generador típico almacenaría (copiar/mover o simplemente almacenar la dirección, ya que el tiempo de vida del argumento cruza el punto de suspensión dentro de co_await) su argumento en el objeto generador y devolvería std::suspend_always, transfiriendo el control al llamante/renaudador.
#include <coroutine> #include <exception> #include <iostream> template<typename T> struct Generador { // El nombre de clase 'Generador' es nuestra elección y // y no es necesario para la corrutina. // El compilador reconoce la corrutina por la presencia de la palabra clave 'co_yield'. // Puede usar el nombre 'MiGenerador' (o cualquier otro) // siempre que incluya la estructura anidada promise_type // con el método 'MiGenerador get_return_object()' . //(Nota: tiene que ajustar los nombres de los constructores/destructor de la clase // cuando elija renombrar la clase) struct promise_type; using handle_type = std::coroutine_handle<promise_type>; struct promise_type {// requerido T valor_; std::exception_ptr excepcion_; Generador get_return_object() { return Generador(handle_type::from_promise(*this)); } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() { excepcion_ = std::current_exception(); }// guardar excepción template<std::convertible_to<T> Origen> // concepto C++20 std::suspend_always yield_value(Origen &&origen) { valor_ = std::forward<Origen>(origen);// almacenar el resultado en promesa return {}; } void return_void() {} }; handle_type h_; Generador(handle_type h) : h_(h) {} ~Generador() { h_.destroy(); } explicit operator bool() { llenar();// La única manera de averiguar de forma confiable si terminamos o no la // corrutina, si se generará o no un valor siguiente (co_yield) en la corrutina // a través del adquiridor de C++ (operator () a continuación) // es ejecutar/reanudar la corrutina hasta el próximo punto co_yield (o dejar // que se caiga). // Luego almacenamos el resultado en la promesa para permitir obtenerlo // (operator() a continuación lo toma sin ejecutar la corrutina) return !h_.done(); } T operator()() { llenar(); lleno_ = false;// vamos a sacar el resultado previamente almacenado en caché // para que la promesa vuelva a estar vacía return std::move(h_.promise().valor_); } private: bool lleno_ = false; void llenar() { if (!lleno_) { h_(); if (h_.promise().excepcion_) std::rethrow_exception(h_.promise().excepcion_); //propagar la excepción de rutina en el contexto de llamada lleno_ = true; } } }; Generador<uint64_t> sucesion_fibonacci(unsigned n) { if (n==0) co_return; if (n>94) throw std::runtime_error("Sucesión de Fibonacci demasiado grande. Los elementos se desbordarían."); co_yield 0; if (n==1) co_return; co_yield 1; if (n==2) co_return; uint64_t a=0; uint64_t b=1; for (unsigned i = 2; i < n;i++) { uint64_t s=a+b; co_yield s; a=b; b=s; } } int main() { try { auto gen = susecion_fibonacci(10); //máximo 94 antes del desborde de uint64_t for (int j=0;gen;j++) std::cout << "fib("<<j <<")=" << gen() << '\n'; } catch (const std::exception& ex) { std::cerr << "Excepción: " << ex.what() << '\n'; } catch (...) { std::cerr << "Excepción desconocida.\n"; } }
Salida:
fib(0)=0 fib(1)=1 fib(2)=1 fib(3)=2 fib(4)=3 fib(5)=5 fib(6)=8 fib(7)=13 fib(8)=21 fib(9)=34
[editar] Notas
Macro de Prueba de característica |
---|
__cpp_lib_coroutine
|
[editar] Apoyo de la biblioteca
La biblioteca de apoyo de corrutinas define varios tipos que brindan apoyo de compilación y tiempos de ejecución para corrutinas.
[editar] Enlaces externos
- David Mazières, 2021 - Tutorial on C++20 coroutines