imagen

[Nota del editor de CSDN] Hace dos años, se lanzó oficialmente C++20. En esta versión, los desarrolladores finalmente introdujeron la función coroutine, que puede hacer que el código sea muy limpio, simple y fácil de entender, al tiempo que mantiene el alto rendimiento de asynchronous. Sin embargo, muchos desarrolladores afirmaron sin rodeos que los desarrolladores de bibliotecas utilizan el estándar de corrutina C++, lo cual es muy complicado y nada amigable para los desarrolladores comunes. En este artículo, basado en el estándar coroutine sin pila utilizado por C++20, Qi Yu, un experto técnico sénior en C++, comparte la práctica de aplicación específica y la experiencia de coroutine con ejemplos específicos.


Autor | Qi Yu, Xu Chuanqi, editor de Han Yao       | Tu Min
Producido | CSDN (ID: CSDNnews)

Después de años de deliberación, debate y preparación, las corrutinas finalmente ingresaron al estándar C++20.

imagen


imagen

La corrutina sin pila propuesta y liderada por Microsoft se ha convertido en el estándar de corrutina C++20


Las rutinas no son un concepto nuevo, existen desde hace décadas y existen en muchos otros lenguajes de programación (Python, C#, Go).

Las corrutinas se dividen en dos tipos: corrutinas sin pila y corrutinas apilables. Las corrutinas sin pila se refieren a funciones que se pueden suspender/reanudar, mientras que las corrutinas apilables son equivalentes a subprocesos en modo de usuario. El costo de cambiar entre corrutinas apilables es el costo de cambiar subprocesos en modo usuario, mientras que el costo de cambiar entre corrutinas sin pila es equivalente al costo de las llamadas a funciones.

La diferencia entre corrutinas sin pila y subprocesos: las corrutinas sin pila solo pueden ser llamadas por subprocesos, y no se adelantan a la programación del kernel, mientras que los subprocesos pueden apropiarse de la programación del kernel.

La corrutina C++20 adopta la corrutina sin pila propuesta y dominada por Microsoft (derivada de C#). Muchas personas se oponen a esta característica, los principales inconvenientes incluyen: problemas de rendimiento difíciles de entender, demasiado flexibles causados ​​por la asignación dinámica, etc. Google lanzó una serie de quejas sobre la propuesta y trató de dar una solución con stack coroutines. Las corrutinas apiladas son mucho más ligeras que los subprocesos a nivel de sistema, pero aún mucho peores que las corrutinas sin pila.

Dado que la filosofía de diseño de C++ es " Cero abstracciones generales ", las corrutinas sin pila finalmente se convirtieron en el estándar de corrutina C++20.

Los dos temas principales de la evolución del mundo de C++ en la actualidad son la asincronía y el paralelismo . La corrutina C++20 puede escribir código asíncrono en sintaxis síncrona, lo que la convierte en una buena herramienta para escribir código asíncrono.La corrutina de bibliotecas asíncronas será la tendencia general, por lo que es necesario dominar las corrutinas C++20.

Mostremos la "belleza" de las rutinas con un ejemplo simple.

async_resolve({host, port}, [](auto endpoint){
  async_connect(endpoint, [](auto error_code){
    async_handle_shake([](auto error_code){
        send_data_ = build_request();

        async_write(send_data_, [](auto error_code){
            async_read();
        });
    });
    });
});

void async_read() {
    async_read(response_, [](auto error_code){
        if(!finished()) {
            append_response(recieve_data_);
            async_read();
        }else {
            std::cout<<"finished ok\n";
        }
    });
}

Pseudocódigo para cliente asíncrono basado en devolución de llamada

El proceso del cliente basado en la devolución de llamada asincrónica es el siguiente:

  • Resolución asíncrona de nombres de dominio

  • Conexión asíncrona

  • Protocolo de enlace SSL asíncrono

  • Enviar datos de forma asíncrona

  • Recibir datos de forma asíncrona

Hay muchas funciones de devolución de llamada en este código, y existen algunas trampas al usar devoluciones de llamada, como cómo garantizar devoluciones de llamada seguras, cómo hacer que las lecturas asincrónicas realicen llamadas recursivas asincrónicas, si se combina con la lógica empresarial asíncrona, el nivel de anidamiento de las devoluciones de llamada será más profundo, hemos visto ¡A la sombra del infierno de devolución de llamada! Algunos lectores pueden pensar que este nivel de devolución de llamada asincrónica es aceptable, pero si el proyecto se vuelve más grande, la lógica comercial se vuelve más compleja y el nivel de devolución de llamada se vuelve más y más profundo, será difícil de mantener.

Echemos un vistazo a cómo escribir este código con rutinas:

auto endpoint = co_await async_query({host, port});
auto error_code = co_await async_connect(endpoint);
error_code = co_await async_handle_shake();
send_data = build_request();
error_code = co_await async_write(send_data);
while(true) {
    co_await async_read(response);
    if(finished()) {
        std::cout<<"finished ok\n";
        break;
    }

    append_response(recieve_data_);
}

Cliente asíncrono basado en corrutina C++20

También es un cliente asíncrono. En comparación con el cliente asíncrono en el modo de devolución de llamada, todo el código es muy refrescante, simple y fácil de entender, mientras mantiene el alto rendimiento de las rutinas asíncronas. ¡Este es el poder de las corrutinas C++20!

Creo que después de leer este ejemplo, ya no querrá escribir código con devoluciones de llamada asincrónicas. ¡Es hora de adoptar las corrutinas!


imagen

¿Por qué C++ 20 elige corrutinas sin pila?


La implementación habitual de las corrutinas apiladas es asignar un gran espacio de memoria (como 64 K) por adelantado en el montón, que es la llamada "pila" de corrutinas. Los parámetros, las direcciones de retorno, etc. se pueden almacenar en esta "pila ". "En el espacio. Si se requiere un cambio de rutina, se puede hacer que el sistema piense que el espacio en el montón es una pila ordinaria por medio de swapcontext, que realiza el cambio de contexto.

La mayor ventaja de una corrutina de pila es que es menos intrusiva y muy fácil de usar. El código comercial existente apenas necesita ser modificado, pero C++20 finalmente optó por usar una corrutina sin pila, principalmente debido a los siguientes aspectos a considerar .

  • límite de espacio de pila

El espacio de "pila" de una rutina de pila es generalmente relativamente pequeño, y existe el riesgo de que se desborde la pila en uso; y si el espacio de "pila" se vuelve grande, es una gran pérdida de espacio de memoria. Las corrutinas sin pila no tienen estas limitaciones, no hay riesgo de desbordamiento y no hay necesidad de preocuparse por la utilización de la memoria.

  • actuación

Las corrutinas apiladas son de hecho más livianas que los subprocesos del sistema al cambiar, pero aún son más pesadas que las corrutinas no apiladas, aunque esto tiene poco impacto en nuestro uso real actual (el uso de sistemas asincrónicos suele ir acompañado de IO, que es de varios órdenes de magnitud más costoso que cambiar), pero también determina que las corrutinas sin pila se pueden usar en algunos escenarios más interesantes. Por ejemplo, Gor Nishanov, el autor de la propuesta de corrutinas C++20, demostró en CppCon 2018 que las corrutinas sin pila pueden cambiar en nanosegundos y, en función de esta función, se realiza la función de reducción de Cache Miss.

Una corrutina sin pila es una generalización de una función ordinaria

Una corrutina sin pila es una función que se puede pausar y reanudar, una generalización de las llamadas a funciones.

¿Por qué?

Sabemos que el cuerpo de la función de una función se ejecuta secuencialmente. Una vez que se completa la ejecución, el resultado se devuelve a la persona que llama. No hay forma de suspenderlo y reanudarlo más tarde, sino esperar a que termine. La rutina sin pila nos permite suspender la función y luego reanudar y ejecutar el cuerpo de la función en cualquier momento que sea necesario. En comparación con las funciones ordinarias, el cuerpo de la función de la rutina puede suspenderse y reanudar la ejecución en cualquier momento.

imagen

Entonces, desde este punto de vista, las corrutinas sin pila son generalizaciones de funciones ordinarias.


imagen

"Pequeñas palabras" de C++20 Coroutine


C++20 proporciona tres nuevas palabras clave (co_await, co_yield y co_return), si una de estas tres palabras clave existe en una función, es una rutina.

El compilador genera una gran cantidad de código para que las corrutinas implementen la semántica de las corrutinas. ¿Qué tipo de código se generará? ¿Cómo implementamos la semántica de coroutines? ¿Cómo se crea la rutina? ¿Qué es el mecanismo co_await? Antes de explorar estos problemas, echemos un vistazo a algunos conceptos básicos relacionados con las corrutinas de C++20.

Objetos relacionados con rutinas

marco de rutina

Cuando la persona que llama llama a una corrutina, primero creará un marco de corrutina, el marco de la corrutina construirá un objeto de promesa y luego generará un objeto de retorno a través del objeto de promesa.

Los principales contenidos del marco coroutine son los siguientes:

  • Parámetros de rutina

  • variable local

  • objeto de promesa

Estos contenidos son necesarios cuando se reanuda la ejecución de la corrutina. La persona que llama accede al marco de la corrutina a través del identificador std::coroutine_handle del marco de la corrutina.

tipo_promesa

tipo_promesa es el tipo del objeto de promesa. Promise_type se usa para definir el comportamiento de una clase de corrutinas, incluido cómo se crea la corrutina, el comportamiento cuando se inicializa y finaliza la corrutina, el comportamiento cuando ocurre una excepción, el comportamiento de cómo generar un awaiter y el comportamiento de co_retorno, etc. Los objetos de promesa se pueden usar para registrar/almacenar el estado de una instancia de rutina. Cada marco de rutina tiene una correspondencia uno a uno con cada objeto de promesa y cada instancia de rutina.

objeto de retorno de rutina

Es creado por el método promise.get_return_object() Un método de implementación común almacenará coroutine_handle en el objeto de coroutine, de modo que el objeto de retorno tenga la capacidad de acceder a la coroutine.

std::coroutine_handle

El asa del marco de la rutina, que se utiliza principalmente para acceder al marco de la rutina subyacente, restaurar el marco de la rutina y liberar el marco de la rutina.
Los programadores pueden activar rutinas llamando a std::coroutine_handle::resume().

co_await, awaiter, awaitable

  • co_await: operador unario;

  • awaitable: tipos que soportan el operador co_await;

  • awaiter: define los tipos de métodos await_ready, await_suspend y await_resume.

co_await expr generalmente se usa para indicar la espera de que se complete una tarea (que puede o no ser perezosa). Cuando co_await expr, el tipo de expr debe ser awaitable, y la semántica específica de la expresión co_await depende del awaiter generado de acuerdo con awaitable.

Parece que hay muchos objetos relacionados con las corrutinas. Aquí es donde las corrutinas son complejas y flexibles. Puede usar estos objetos para lograr un control total sobre las corrutinas y realizar cualquier idea. Sin embargo, primero debe comprender cómo cooperan estos objetos.Si comprende esto, dominará el principio de las corrutinas y podrá escribir aplicaciones de corrutinas con facilidad.

Cómo funcionan juntos los objetos coroutine

Un código simple que muestra cómo estos objetos coroutine funcionan juntos:

Return_t foo () { 
    auto res = co_await awaiter; 
    co_return res ; 
}

Return_t: objeto de devolución de promesa.

awaiter: Esperando a que se complete una tarea.

imagen

Diagrama de flujo de ejecución de la rutina

El método en la parte azul claro de la figura es la función del objeto de promesa asociado con Return_t, y la parte roja clara es el awaiter que espera co_await.

La conducción de este proceso está impulsada por el código generado por el compilador de acuerdo con la función coroutine, que se divide en tres partes:

  • Creación de rutinas;

  • co_await awaiter espera a que se complete la tarea;

  • Obtenga el valor de retorno de la rutina y libere el marco de la rutina.

Creación de rutinas

Return_t foo () { 
    auto res = co_await awaiter; 
    co_return res ; 
}

La rutina foo() generará el siguiente código de plantilla (pseudocódigo), y la creación de la rutina generará un código similar:

{
  co_await promise.initial_suspend();
  try
  {
    coroutine body;
  }
  catch (...)
  {
    promise.unhandled_exception();
  }
FinalSuspend:
  co_await promise.final_suspend();
}

Primero, se debe crear una corrutina, y si se suspende después de que se crea la corrutina, la persona que llama establece el tipo de devolución de initial_suspend.

El proceso de creación de una rutina es más o menos el siguiente:

  • Crear un marco coroutine

  • Construcción de objetos de promesa en marcos coroutine

  • Copie los parámetros de la corrutina en el marco de la corrutina

  • Llame a promise.get_return_object() para devolver un objeto a la persona que llama, que es el objeto Return_t en el código

Hay algunos puntos personalizables en este marco de plantilla: como initial_suspend, final_suspend, unhandled_exception y return_value.

Podemos controlar si la rutina se suspende a través de los tipos de promesas de retorno initial_suspend y final_suspend, manejar excepciones en unhandled_exception y guardar el valor de retorno de la rutina en return_value.

Puede personalizar los objetos de retorno de initial_suspend y final_suspend según sea necesario para decidir si suspender la rutina. Si se suspende la rutina, el control del código vuelve a la persona que llama; de lo contrario, la ejecución continúa con el cuerpo de la función de rutina.

imagen

También vale la pena señalar que si deshabilita las excepciones, no habrá intentos de captura en el código generado. En este momento, la eficiencia operativa de la rutina es casi la misma que la de la versión sin rutina de la función ordinaria. Esto es importante en escenarios integrados y es uno de los objetivos de diseño de coroutines.

mecanismo co_await

El operador co_await es una nueva palabra clave en C++ 20. Co_await expr generalmente significa esperar una tarea de evaluación diferida. Esta tarea puede ejecutarse en un subproceso o en el kernel del sistema operativo. Se desconoce cuándo finaliza la ejecución. Para el rendimiento, no queremos bloquear y esperar a que se complete la tarea, por lo que usamos co_await para suspender la rutina y devolverla a la persona que llama. La persona que llama puede continuar haciendo cosas. Cuando se completa la tarea, la rutina se reanuda y obtiene el resultado. devuelto por co_await.

Entonces co_await generalmente tiene las siguientes funciones:

  • suspender la rutina;

  • volver a la persona que llama;

  • Devuelve el resultado de la tarea después de esperar a que se complete una tarea (que puede ser diferida o no diferida).

El compilador generará un código como este de acuerdo con co_await expr:

{
  auto&& value = <expr>;
  auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
  auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
  if (!awaiter.await_ready()) //是否需要挂起协程
  {
    using handle_t = std::experimental::coroutine_handle<P>;

    using await_suspend_result_t =
      decltype(awaiter.await_suspend(handle_t::from_promise(p)));

    <suspend-coroutine> //挂起协程

    if constexpr (std::is_void_v<await_suspend_result_t>)
    
{
      awaiter.await_suspend(handle_t::from_promise(p)); //异步(也可能同步)执行task
      <return-to-caller-or-resumer> //返回给caller
    }
    else
    {
      static_assert(
         std::is_same_v<await_suspend_result_tbool>,
         "await_suspend() must return 'void' or 'bool'.");

      if (awaiter.await_suspend(handle_t::from_promise(p)))
      {
        <return-to-caller-or-resumer>
      }
    }

    <resume-point> //task执行完成,恢复协程,这里是协程恢复执行的地方
  }

  return awaiter.await_resume(); //返回task结果
}

Este proceso de ejecución de código es la parte rosa del "Diagrama de flujo de ejecución de rutina". Como puede ver en este código generado, puede controlar si suspender la rutina o continuar con la ejecución al personalizar el valor de retorno de awaiter. return false La corrutina se suspenderá y se ejecutará awaiter.await_suspend, y el valor de retorno de awaiter.await_suspend determinará si se debe volver a la persona que llama o continuar con la ejecución.

Este mecanismo de co_await es la clave para cambiar "devolución de llamada asíncrona" a "sincronización".

Los dos objetos más importantes en la corrutina de C++20 son el objeto promesa (recuperar la corrutina y obtener el resultado de ejecución de una tarea) y awaiter (suspender la corrutina y esperar a que se complete la tarea), los demás son "herramientas". ”, para lograr la rutina deseada, la clave es diseñar cómo hacer que estos dos objetos cooperen bien.

Para obtener más detalles sobre co_await, los lectores pueden consultar este documento ( https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await ).

Pequeñas palabras y rectitud

Volvamos atrás y veamos esta rutina simple:

Return_t foo () { 
    auto res = co_await awaiter; 
    co_return res ; 
}

La rutina foo tiene solo tres líneas de código, pero finalmente genera más de 100 líneas de código. Por ejemplo, la creación de la rutina y el mecanismo co_await están todos implementados por estos códigos. ".

Se ha dicho mucho sobre el concepto y el principio de implementación de las corrutinas de C++ 20. A continuación, se utiliza un ejemplo simple de corrutina de C++ 20 para mostrar cómo funcionan las corrutinas.


imagen

Un ejemplo simple de corrutina C++20


Este ejemplo es muy simple, envíe la corrutina a un hilo a través de co_await e imprima la identificación del hilo.

#include <coroutine>
#include <iostream>
#include <thread>

namespace Coroutine {
  struct task {
    struct promise_type {
      promise_type() {
        std::cout << "1.create promie object\n";
      }
      task get_return_object() {
        std::cout << "2.create coroutine return object, and the coroutine is created now\n";
        return {std::coroutine_handle<task::promise_type>::from_promise(*this)};
      }
      std::suspend_never initial_suspend() {
        std::cout << "3.do you want to susupend the current coroutine?\n";
        std::cout << "4.don't suspend because return std::suspend_never, so continue to execute coroutine body\n";
        return {};
      }
      std::suspend_never final_suspend() noexcept {
        std::cout << "13.coroutine body finished, do you want to susupend the current coroutine?\n";
        std::cout << "14.don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye\n";
        return {};
      }
      void return_void() {
        std::cout << "12.coroutine don't return value, so return_void is called\n";
      }
      void unhandled_exception() {}
    };

    std::coroutine_handle<task::promise_type> handle_;
  };

  struct awaiter {
    bool await_ready() {
      std::cout << "6.do you want to suspend current coroutine?\n";
      std::cout << "7.yes, suspend becase awaiter.await_ready() return false\n";
      return false;
    }
    void await_suspend(
      std::coroutine_handle<task::promise_type> handle)
 
{
      std::cout << "8.execute awaiter.await_suspend()\n";
      std::thread([handle]() mutable { handle(); }).detach();
      std::cout << "9.a new thread lauched, and will return back to caller\n";
    }
    void await_resume() {}
  };

  task test() {
    std::cout << "5.begin to execute coroutine body, the thread id=" << std::this_thread::get_id() << "\n";//#1
    co_await awaiter{};
    std::cout << "11.coroutine resumed, continue execcute coroutine body now, the thread id=" << std::this_thread::get_id() << "\n";//#3
  }
}// namespace Coroutine

int main() {
  Coroutine::test();
  std::cout << "10.come back to caller becuase of co_await awaiter\n";
  std::this_thread::sleep_for(std::chrono::seconds(1));

  return 0;
}

Salida de prueba:

1.create promie object
2.create coroutine return object, and the coroutine is created now
3.do you want to susupend the current coroutine?
4.don't suspend because return std::suspend_never, so continue to execute coroutine body
5.begin to execute coroutine body, the thread id=0x10e1c1dc0
6.do you want to suspend current coroutine?
7.yes, suspend becase awaiter.await_ready() return false
8.execute awaiter.await_suspend()
9.new thread lauched, and will return back to caller
10.come back to caller becuase of co_await awaiter
11.coroutine resumed, continue execcute coroutine body now, the thread id=0x700001dc7000
12.coroutine don't return value, so return_void is called
13.coroutine body finished, do you want to susupend the current coroutine?
14.don'
suspend because return std::suspend_never, and the continue will be automatically destroyed, bye

A partir de este resultado, puede ver claramente cómo se crea la corrutina, co_await espera el final del subproceso, el valor de retorno de la corrutina después de que finaliza el subproceso y todo el proceso de destrucción de la corrutina.

Creación de rutinas

1, 2 y 3 en el resultado muestran el proceso de creación de la rutina, primero crea una promesa y luego devuelve la tarea a través de promise.get_return_object(), luego se crea la rutina.

Comportamiento después de la creación de la rutina

Después de crear la rutina, ¿debería ejecutarse la función de rutina inmediatamente? ¿O colgar primero? Este comportamiento está determinado por promise.initial_suspend(), ya que devuelve un awaiter de std::suspend_never, no suspenderá la rutina, por lo que la función de rutina se ejecuta de inmediato.

co_await awaiter

执行协程到函数的 co_await awaiter 时,是否需要等待某个任务?返回 false 表明希望等待,于是接着进入到 awaiter.wait_suspend(),并挂起协程,在 await_suspend 中创建了一个线程去执行任务(注意协程具柄传入到线程中了,以便后面在线程中恢复协程),之后就返回到 caller了,caller 这时候可以不用阻塞等待线程结束,可以做其它事情。注意:这里的 awaiter 同时也是一个 awaitable,因为它支持 co_await。

更多时候我们在线程完成之后才去恢复协程,这样可以告诉挂起等待任务完成的协程:任务已经完成了,现在可以恢复了,协程恢复后拿到任务的结果继续执行。

协程恢复

当线程开始运行的时候恢复挂起的协程,这时候代码执行会回到协程函数继续执行,这就是最终的目标:在一个新线程中去执行协程函数的打印语句。

协程销毁

awaiter.final_suspend 决定是否要自动销毁协程,返回 std::suspend_never 就自动销毁协程,否则需要用户手动去销毁。

协程的“魔法”

再回过头来看协程函数:

task test() {
    std::cout << std::this_thread::get_id() << "\n";
    co_await awaiter{};
    std::cout << std::this_thread::get_id() << "\n";
}
输出结果显示 co_await 上面和下面的线程是不同的,以 co_await 为分界线,co_await 之上的代码在一个线程中执行,co_await 之下的代码在另外一个线程中执行,一个协程函数跨了两个线程,这就是协程的“魔法”。本质是因为在另外一个线程中恢复了协程,恢复后代码的执行就在另外一个线程中了。

另外,这里没有展示如何等待一个协程完成,简单的使用了线程休眠来实现等待的,如果要实现等待协程结束的逻辑,代码还会增加一倍。

相信你通过这个简单的例子对 C++20 协程的运行机制有了更深入的理解,同时也会感叹,协程的使用真的只适合库作者,普通的开发者想用 C++20 协程还是挺难的,这时就需要协程库了,协程库可以大幅降低使用协程的难度。


imagen

为什么需要一个协程库


通过前面的介绍可以看到,C++20 协程还是比较复杂的,它的概念多、细节多,又是编译器生成的模板框架,又是一些可定制点,需要了解如何和编译器生成的模板框架协作,这些对于普通的使用者来说光理解就比较吃力,更逞论灵活运用了。

这时也可以理解为什么当初 Google 吐槽这样的协程提案难于理解、过于灵活了,然而它的确可以让我们仅需要通过定制化一些特定方法就可以随心所欲的控制协程,还是很灵活的。

总之,这就是 C++20 协程,它目前只适合给库作者使用,因为它只提供了一些底层的协程原语和一些协程暂停和恢复的机制,普通用户如果希望使用协程只能依赖协程库,由协程库来屏蔽这些底层细节,提供简单易用的 API。因此,我们迫切需要一个基于 C++20 协程封装好的简单易用的协程库。

正是在这种背景下,C++20 协程库 async_simple(https://github.com/alibaba/async_simple)就应运而生了!

阿里巴巴开发的 C++20 协程库,目前广泛应用于图计算引擎、时序数据库、搜索引擎等在线系统。连续两年经历天猫双十一磨砺,承担了亿级别流量洪峰,具备非常强劲的性能和可靠的稳定性。

async_simple 现在已经在 GitHub 上开源,有了它你在也不用为 C++20 协程的复杂而苦恼了,正如它的名字一样,让异步变得简单。

接下来我们将介绍如何使用 async_simple 来简化异步编程。


imagen

async_simple 让协程变得简单


async_simple 提供了丰富的协程组件和简单易用的 API,主要有:

  1. Lazy:lazy 求值的无栈协程

  2. Executor:协程执行器

  3. 批量操作协程的 API:collectAll 和 collectAny

  4. uthread:有栈协程

关于 async_simple 的更多介绍和示例,可以看 GitHub(https://github.com/alibaba/async_simple/tree/main/docs/docs.cn)上的文档。

有了这些常用的丰富的协程组件,我们写异步程序就变得很简单了,通过之前打印线程 id 例子来展示如何使用 async_simple 来实现它,也可以对比下用协程库的话,代码会简单多少。

#include "async_simple/coro/Lazy.h"
#include "async_simple/executors/SimpleExecutor.h"

Lazy<void> PrintThreadId(){
    std::cout<<"thread id="<<std::this_thread::get_id()<<"\n";
    co_return;
}

Lazy<void> TestPrintThreadId(async_simple::executors::SimpleExecutor &executor){
    std::cout<<"thread id="<<std::this_thread::get_id()<<"\n";
    PrintThreadId().via(&executor).detach();
    co_return;
}

int main() {
    async_simple::executors::SimpleExecutor executor(/*thread_num=*/1);
    async_simple::coro::syncAwait(TestPrintThreadId(executor));
    return 0;
}

借助 async_simple 可以轻松地把协程调度到 executor 线程中执行,整个代码变得非常清爽,简单易懂,代码量相比之前少得多,用户也不用去关心 C++20 协程的诸多细节了。

借助 async_simple 这个协程库,可以轻松的让 C++20 协程这只“王谢堂前燕,飞入寻常百姓家”!

async_simple 提供了很多 example,比如使用 async_simple 开发 http client、http server、smtp client 等示例,更多 Demo 可以看 async_simple 的 demo example(https://github.com/alibaba/async_simple/blob/main/demo_example)。


imagen

性能


使用 async_simple 中的 Lazy 与 folly 中的 Task 以及 cppcoro 中的 task 进行比较,对无栈协程的创建速度与切换速度进行性能测试。需要说明的是,这只是一个高度裁剪的测试用于简单展示 async_simple,并不做任何性能比较的目的。而且 Folly::Task 有着更多的功能,例如 Folly::Task 在切换时会在 AsyncStack 记录上下文以增强程序的 Debug 便利性。

测试硬件

CPU: Intel® Xeon® Platinum 8163 CPU @ 2.50GHz

测试结果

单位: 纳秒,数值越低越好。

imagen

imagen

测试结果表明 async_simple 的性能还是比较出色的,未来还会持续去优化改进。


imagen

总结


C++20 协程像一台精巧的“机器”,虽然复杂,但非常灵活,允许我们去定制化它的一些“零件”,通过这些定制化的“零件”我们可以随心所欲的控制这台“机器”,让它帮我们实现任何想法。

正是这种复杂性和灵活性让 C++20 协程的使用变得困难,幸运的是我们可以使用工业级的成熟易用的协程库 async_simple 来简化协程的使用,让异步变得简单!

参考资料:

  • https://github.com/alibaba/async_simple

  • https://timsong-cpp.github.io/cppwp/n4868/

  • https://blog.panicsoftware.com/coroutines-introduction/

  • https://lewissbaker.github.io/

  • https://juejin.cn/post/6844903715099377672

  • https://wiki.tum.de/download/attachments/93291100/Kolb%20report%20-%20Coroutines%20in%20C%2B%2B20.pdf

作者:祁宇,Modern C++ 开源社区 purecpp.org 创始人,《深入应用 C++11》作者
许传奇,阿里巴巴开发工程师, LLVM Committer, C++ 标准委员会成员
韩垚,阿里巴巴工程师,目前从事搜索推荐引擎开发

imagen

END

新程序员001-004》全面上市,对话世界级大师,报道中国IT行业创新创造


imagen

— 推荐阅读 —
☞Lei Jun renunció como director de Xiaomi Youpin y aún posee el 70% de las acciones; Musk decidió no unirse a la junta de Twitter; Se anunció el código fuente del kernel OnePlus 10 Pro | Geek Headlines
☞ ¿Actualización de vulnerabilidad? Los piratas informáticos pueden implementar el malware Mirai utilizando la falla de Spring Framework
☞ "¡En represalia por el despido de la empresa, cambié todos los comentarios de código del proyecto!"

—Haz clic aquí ↓↓↓ Recuerda prestar atención a las estrellas~  

"Compartir", "Me gusta" y "Ver" con un solo clic

Lograr 100 millones de técnicos

imagen