传统异步回调 vs C++20协程
协程是一种函数对象,可以设置锚点做暂停,然后再该锚点恢复继续运行。它是如何应用在网络异步编程方面的,请对比下面的两种代码风格:
基于回调的异步网络编程
先来看一个异步编程的典型例子 (伪代码):
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"; } });}
基于异步回调的 client 流程如下:
- 异步域名解析
- 异步连接
- 异步 SSL 握手
- 异步发送数据
- 异步接收数
这个代码有很多回调函数,使用回调的时候还有一些陷阱,比如如何保证安全的回调、如何让异步读实现异步递归调用,如果再结合异步业务逻辑,回调的嵌套层次会更深,我们已经看到callback hell 的影子了!可能也有读者觉得这个程度的异步回调还可以接受,但是如果工程变大,业务逻辑变得更加复杂,回调层次越来越深,维护起来就很困难了。
基于协程的异步网络编程
再来看看用协程是怎么写同样的逻辑 (伪代码):
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_);}
同样是异步 client,相比回调模式的异步 client,整个代码非常清爽,简单易懂,同时保持了异步的高性能,这就是 C++20 协程的威力!
C++ 20 协程提案之争
协程分为无栈协程和有栈协程两种
- 无栈指可挂起/恢复的函数
- 有栈协程则相当于用户态线程
有栈协程切换的成本是用户态线程切换的成本,而无栈协程切换的成本则相当于函数调用的成本。
有栈(stackful)协程通常的实现手段是在堆上提前分配一块较大的内存空间(比如 64K),也就是协程所谓的“栈”,参数、return address 等都可以存放在这个“栈”空间上。如果需要协程切换,那么通过 swapcontext 一类的形式来让系统认为这个堆上空间就是普通的栈,这就实现了上下文的切换。
有栈协程最大的优势就是侵入性小,使用起来非常简便,已有的业务代码几乎不需要做什么修改,但是 C++20 最终还是选择了使用无栈协程,主要出于下面这几个方面的考虑:
栈空间的限制
有栈协程的“栈”空间普遍是比较小的,在使用中有栈溢出的风险;而如果让“栈”空间变得很大,对内存空间又是很大的浪费。无栈协程则没有这些限制,既没有溢出的风险,也无需担心内存利用率的问题。
性能
有栈协程在切换时确实比系统线程要轻量,但是和无栈协程相比仍然是偏重的,这一点虽然在我们目前的实际使用中影响没有那么大,但也决定了无栈协程可以用在一些更有意思的场景上。举个例子,C++20 coroutines 提案的作者Gor Nishanov 在 CppCon 2018 上演示了无栈协程能做到纳秒级的切换,并基于这个特点实现了减少 Cache Miss 的特性。
无栈协程是普通函数的泛化
无栈协程是一个可以暂停和恢复的函数,是函数调用的泛化。
我们知道一个函数的函数体 (function body) 是顺序执行的,执行完之后将结果返回给调用者,我们没办法挂起它并稍后恢复它,只能等待它结束。而无栈协程则允许我们把函数挂起,然后在任意需要的时刻去恢复并执行函数体,相比普通函数,协程的函数体可以挂起并在任意时刻恢复执行。从这个角度来说,无栈协程是普通函数的泛化。

无栈协程原理
有栈协程:每个协程创建的时候都会获得一块固定大小 (如 128k) 的堆内存,协程运行的时候就是使用这块堆内存当作运行栈使用,切换时候保存/恢复运行栈和相应寄存器
无栈协程:实现原理并不是通过切换时保存/恢复运行栈和寄存器实现的,它的实现见下,由于协程的每个中断点都是确定,那其实只需要将函数的代码再进行细分,保存好局部变量,做好调用过程的状态变化。例如:
void fn(){int a, b, c;a = b + c;yield();b = c + a;yield();c = a + b;}
将上面的代码自动转换为以下形式:
Struct fn{ int a, b, c; int __state = 0; void resume(){ switch(__state) { case 0: return fn1(); case 1: return fn2(); case 2: return fn3(); } } void fn1(){ a = b + c; } void fn2(){ b = c + a; } void fn3(){ c = a + b; }};
上面就将一个协程函数 fn 进行切分后变成一个Struct,这样的实现相对于有栈协程而言使用的内存更少。当然上面只是一种演示,对应早期的 reenter 用法,这个宏底层通过 switch-case 将函数拆分成多个可重入点,一般也称为 duff device。
C++20 协程缺点
难于理解、过于灵活、动态分配导致的性能问题等等。
C++20 协程关键概念繁多:
- 协程帧 (coroutine frame)
- 协程参数
- 局部变量
- promise 对象
- promise_type
- coroutine return object
- std::coroutine_handle
- co_await、awaiter、awaitable
C++20 是通过 Compiler 代码生成与语法糖配合的模式来实现的相关机制,与其它语言对比之下 C++20 协程使用的直观度,便利性都会存在一些折扣。
C++ 20 协程概览图:

C++20 协程运行流程图:

另一个视角:

await 流程:

目前只适合给库作者使用,因为它只提供了一些底层的协程原语和一些协程暂停和恢复的机制,普通用户如果希望使用协程只能依赖协程库,由协程库来屏蔽这些底层细节,提供简单易用的 API,以便业务侧使用负担尽可能低。
协程库
选取一个合适的协程库有助于屏蔽 C++20 底层的实现细节,对用户更加友好,目前市面上有以下几种选择:
- boost::asio
- coroutine / reenter / yield / fork
- spawn / strand / yield_context
- (C++20) io_context / executor / co_spawn / co_await / co_return / use_awaitable / executor
- boost::coroutine2 [有栈协程]
- coroutine<>::pull_type / coroutine<>::push_type / coro_back / sink
- boost::fiber [coroutine2 + 协程调度器 + 协程同步工具]
- fiber / buffered_channel<> / barrier / mutex / channel / promise / future / condition_variable / sleep / yield
- cppcoro [C++20]
- async_simple [阿里]
- libco [腾讯,有栈协程]
- libcopp [有栈协程]
接入
编译参数:
- -std=c++2a
- -fcoroutines-ts
- -DASIO_STA_ALONE
编译器厂商支持情况:
- gcc 10
- msvc
- 1900 (VS2015 14.0 部分支持)
- 1910 (VS2017 15.0 ts 支持)
- 1928 (VS2019 16.8)
- clang 8 (部分支持)
C++20 四大新增特性
- 概念 (concept)
- 范围 (ranges)
- 协程 (coroutine)
- 模块 (modules)
实例
基于 boost::asio C++20 协程实现的 echo 服务:
#include <asio/co_spawn.hpp>#include <asio/detached.hpp>#include <asio/io_context.hpp>#include <asio/ip/tcp.hpp>#include <asio/signal_set.hpp>#include <asio/write.hpp>#include <cstdio>#include <iostream>using asio::ip::tcp;using asio::awaitable;using asio::co_spawn;using asio::detached;using asio::use_awaitable;namespace this_coro = asio::this_coro;#if defined(ASIO_ENABLE_HANDLER_TRACKING)# define use_awaitable \ asio::use_awaitable_t(__FILE__, __LINE__, __PRETTY_FUNCTION__)#endifawaitable<void> echo(tcp::socket socket){ try { char data[1024]; for (;;) { std::size_t n = co_await socket.async_read_some(asio::buffer(data), use_awaitable); co_await async_write(socket, asio::buffer(data, n), use_awaitable); } } catch (std::exception& e) { std::printf("echo Exception: %s\n", e.what()); }}void fn2(){ std::cout<<"hhh\n";}void fn(){ fn2();}awaitable<void> listener(){ auto executor = co_await this_coro::executor; fn(); tcp::acceptor acceptor(executor, {tcp::v4(), 8988}); for (;;) { tcp::socket socket = co_await acceptor.async_accept(use_awaitable); //调用协程,体现同步性 co_spawn(executor, echo(std::move(socket)), detached);// 创建连接处理线程 }}int main(){ try { asio::io_context io_context(1); asio::signal_set signals(io_context, SIGINT, SIGTERM); signals.async_wait([&](auto, auto){ io_context.stop(); }); co_spawn(io_context, listener(), detached); // 创建纤程,体现并发性 io_context.run();// 开始调度 } catch (std::exception& e) { std::printf("Exception: %s\n", e.what()); }}
参考
[2].C++20协程原理和应用
[3].C++网络编程之asio(五)——在asio中使用协程
[4].C++20协程不完全指南
[5].深入浅出c++协程
[7].聊聊协程的发展历程
[8].asio服务器模式:协程
[10].Boost中的协程—Boost.Asio中的coroutine类
[11].如何在C++17中实现stackless coroutine以及相关的任务调度器
[12].C++20 Coroutine实例教学
[13].译:你的第一个协程程序(Your first coroutine)
[14].ASIO 与协程
[15].C++ compiler support