Saltar a contenido

Última actualización:2026-10-12

Introducción a las corrutinas de C++20

1.Qué son las Coroutines

A partir de C++20, C++ por fin soporta corrutinas; C++23 debería aportar soporte adicional y cubrir, como mínimo, los casos de uso más habituales.

Una corrutina (en inglés: coroutine) es un tipo de componente de los programas informáticos que generaliza la subrutina propia del multitarea cooperativa, permitiendo que la ejecución se suspenda y se reanude. En comparación con las subrutinas, las corrutinas son más generales y flexibles, aunque en la práctica no se usan tanto como aquellas. Las corrutinas son más adecuadas para implementar componentes de programas bien conocidos, como la multitarea cooperativa, el manejo de excepciones, el bucle de eventos, los iteradores, las listas infinitas y las tuberías.

Un párrafo procedente de Wikipedia que describe la idea básica de las corrutinas.

Desde la perspectiva de C++, puede entenderse de forma sencilla que una corrutina es cualquier función que contenga co_return, co_yield o co_await.

Hoy en día, las corrutinas de C++20 son azúcar sintáctico sobre objetos función: el compilador genera alrededor de la corrutina un andamiaje de código. Este código depende de los promise types definidos por el usuario. Es posible que esto cambie a partir de C++23, pero hasta entonces nos atenemos a las reglas de C++20 y esos tipos hay que escribirlos uno mismo.

Antes he mencionado co_return, co_yield y co_await, que se explicarán en entregas futuras.

2.Step By Step Coroutine

A continuación partimos de un ejemplo de coroutine sencillo: la corrutina siguiente no hace absolutamente nada; la llamaremos la corrutina HelloTask.

#include <coroutine>
struct HelloTask {
    struct promise_type {
        HelloTask get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};
HelloTask myCoroutine() {
    co_return; // make it a coroutine
}
int main() {
    HelloTask x = myCoroutine();
}

Compila y ejecuta este código:

g++ hello.cpp -fcoroutines -std=c++20

Necesitas que tu compilador soporte las características de C++20.

Recorramos de arriba abajo este programa HelloWorld y veamos en qué se diferencia del desarrollo habitual.

2.1 Archivo de cabecera

Lo primero, el archivo de cabecera incluye coroutine; aquí tomo directamente el código fuente de gcc-10 para ver qué hace exactamente.

https://github.com/gcc-mirror/gcc/blob/releases/gcc-10/libstdc++-v3/include/std/coroutine

Su contenido es el siguiente:

// 用于发现协程promise_type的特征类型
// 核心代码:using promise_type = typename _Result::promise_type;
1.coroutine_traits

// 协程句柄,用于管理暂停或执行的协程以及Promise
2.coroutine_handle
  // coroutine_handle全特化版本  是其他特化的公开基类。它保有handle的底层     地址。
  2.1 coroutine_handle<void>

  // coroutine_handle泛化版本
  2.2 template <typename _Promise>
      struct coroutine_handle : coroutine_handle<>


  // coroutine_handle全特化版本 处理无操作协程
  2.3 template <>
      struct coroutine_handle<noop_coroutine_promise> : public          coroutine_handle<>
3.平凡多可等待对象 Trivial awaitables
  // await表达式应该暂停
  3.1 suspend_always
  // await表达式不应该暂停
  3.2 suspend_never

Puede verse que incluye muchos términos específicos:

  • coroutine_handle
  • Promise
  • awaitable

Etcétera.

Todo el contenido sobre corrutinas que veremos después gira en torno a esto.

2.2 Promise

Siguiendo con el HelloTask anterior, encontramos este fragmento:

struct HelloTask {
    struct promise_type {
        HelloTask get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};

Entonces la pregunta es: ¿por qué hay que escribir aquí el promise_type?

La razón de escribir promise_type está en el handle mencionado en la sección 2.1: cuando el compilador crea el handle de la corrutina, necesita especificar el promise_type de ese objeto corrutina, por eso casi todos los objetos corrutina tienen una estructura HelloTask::promise_type. Quienes sean observadores ya lo habrán entendido: aquí se pasa el promise_type, dentro del Handle se analiza, y así se determina el comportamiento del objeto corrutina definido por el usuario.

Veamos más a fondo qué hay dentro de promise_type: su contenido es fijo; a medida que avancemos y haya más contenido, irán apareciendo más funciones; por ejemplo, aquí se usa return_void, pero cuando más adelante co_return devuelva un objeto, se usará otra función.

get_return_object puede devolver el objeto personalizado actual; aquí no hace nada, y retomando la sección 2.1, podemos hacerlo con coroutine_handle.

struct promise_type {
    using Handle = std::coroutine_handle<promise_type>;
    HelloTask get_return_object() {
        return HelloTask{Handle::from_promise(*this)};
    }
  // other member function
}

Se ve que aquí se usa el miembro from_promise de coroutine_handle para construir un objeto.

initial_suspend: indica al compilador si la corrutina debe permanecer suspendida en su primera ejecución, o expresado de otro modo, si debe permanecer suspendida al instanciarse la corrutina.

final_suspend: lo contrario de initial_suspend; al completarse el cuerpo de la corrutina, al final de la corrutina hay un final_suspend. Esto da la oportunidad de hacer limpieza adicional.

Ambos devuelven un awaitable; aquí se ha usado suspend_always de la cabecera, aunque también puede ser un awaitable definido por el usuario, algo que se explicará en artículos posteriores.

El código fuente de suspend_always y suspend_never es el siguiente:

struct suspend_always
{
  constexpr bool await_ready() const noexcept { return false; }

  constexpr void await_suspend(coroutine_handle<>) const noexcept {}

  constexpr void await_resume() const noexcept {}
};

struct suspend_never
{
  constexpr bool await_ready() const noexcept { return true; }

  constexpr void await_suspend(coroutine_handle<>) const noexcept {}

  constexpr void await_resume() const noexcept {}
};

Esto incluye varios miembros:

  • await_ready
  • Si ya se sabe que el resultado está listo, ¿se completa de forma síncrona?
  • await_suspend
  • El valor de retorno puede ser bool, void o un coroutine handle.
  • Aquí se devuelve void, con lo cual el control vuelve inmediatamente al caller/resumer de la corrutina actual (esta corrutina sigue suspendida); en cambio, si await_suspend devuelve bool, true significa que el control pasa al caller/resumer, y false significa que se reanuda la corrutina actual. Lo del retorno de un coroutine handle se explicará en artículos posteriores.
  • await_resume
  • Su resultado es el resultado de toda la expresión co_await. Por eso el valor de retorno no tiene por qué ser void; también puede ser un objeto personalizado o similar.

return_void: cuando se usa co_return;, se invoca return_void.

unhandled_exception: se usa para capturar excepciones.

Por último, falta un paso: crear la coroutine; según la definición inicial, una corrutina es cualquier función que contenga co_return, co_yield o co_await. De ahí surge:

HelloTask myCoroutine() {
    co_return; // make it a coroutine
}

2.3 Flujo completo

¿Cuál es el flujo de invocación completo? En esta sección explicamos en detalle la lógica de ejecución del código anterior; antes de empezar, modificamos el código añadiendo algunas operaciones de print:

#include <iostream>
#include <coroutine>
struct HelloTask {
    struct promise_type {
        HelloTask get_return_object() { std::cout << "get_return_object" << std::endl; return { }; }
        std::suspend_never initial_suspend() { std::cout << "initial_suspend" << std::endl; return {}; }
        std::suspend_never final_suspend() noexcept { std::cout << "final_suspend" << std::endl;return {}; }
        void return_void() { std::cout << "return_void" << std::endl; }
        void unhandled_exception() { std::cout << "unhandled_exception" << std::endl; }
    };
};
HelloTask myCoroutine() {
    std::cout << "create coroutine" << std::endl;
    co_return; // make it a coroutine
}
int main() {
    std::cout << "start main" << std::endl;
    HelloTask x = myCoroutine();
    std::cout << "end main" << std::endl;
}

La salida es:

start main
get_return_object
initial_suspend
create coroutine
return_void
final_suspend
end main

El flujo es el siguiente:

  • start main
  • Se asigna el objeto coutine en el heap, se construye la promise y se llama a promise.get_return_object, que devuelve el objeto actual (en este momento se muestra get_return_object); se hace así para guardar el resultado en una variable local.
  • promise.initial_suspend, en este momento se muestra initial_suspend; aquí se dispara el co_await, se muestra create coroutine, se ejecuta el cuerpo de la corrutina y se llama a co_return.
  • co_return llama a promise.return_void y muestra return_void.
  • Finalmente, promise.final_suspend muestra final_suspend.
  • end main

Queda muchísimo contenido relacionado con las corrutinas; esta es la entrega inaugural y la iré actualizando continuamente~

Temas relacionados

Comentarios