传统异步回调 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__)
#endif awaitable<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());
}
}

参考

[1]. 在 Boost.Asio 中使用协程

[2]. C++20协程原理和应用

[3]. C++网络编程之asio(五)——在asio中使用协程

[4]. C++20协程不完全指南

[5]. 深入浅出c++协程

[6]. 协程的原理(Coroutine Theory)

[7]. 聊聊协程的发展历程

[8]. asio服务器模式:协程

[9]. C++中的yield和reenter和fork

[10]. Boost中的协程—Boost.Asio中的coroutine类

[11]. 如何在C++17中实现stackless coroutine以及相关的任务调度器

[12]. C++20 Coroutine实例教学

[13]. 译:你的第一个协程程序(Your first coroutine)

[14]. ASIO 与协程

[15]. C++ compiler support

使用 C++ 20 协程降低异步网络编程复杂度的更多相关文章

  1. 第十一章:Python高级编程-协程和异步IO

    第十一章:Python高级编程-协程和异步IO Python3高级核心技术97讲 笔记 目录 第十一章:Python高级编程-协程和异步IO 11.1 并发.并行.同步.异步.阻塞.非阻塞 11.2 ...

  2. C++20协程实例:携程化的IOCP服务端/客户端

    VC支持协程已经有一段时间了,之前一直想不明白协程的意义在哪里,前几天拉屎的时候突然灵光一闪: 以下是伪代码: task server() { for (;;) { sock_context s = ...

  3. 带你简单了解python协程和异步

    带你简单了解python的协程和异步 前言 对于学习异步的出发点,是写爬虫.从简单爬虫到学会了使用多线程爬虫之后,在翻看别人的博客文章时偶尔会看到异步这一说法.而对于异步的了解实在困扰了我好久好久,看 ...

  4. Python实现基于协程的异步爬虫

    一.课程介绍 1. 课程来源 本课程核心部分来自<500 lines or less>项目,作者是来自 MongoDB 的工程师 A. Jesse Jiryu Davis 与 Python ...

  5. python协程与异步协程

    在前面几个博客中我们一一对应解决了消费者消费的速度跟不上生产者,浪费我们大量的时间去等待的问题,在这里,针对业务逻辑比较耗时间的问题,我们还有除了多进程之外更优的解决方式,那就是协程和异步协程.在引入 ...

  6. 进击的Python【第十章】:Python的高级应用(多进程,进程间通信,协程与异步,牛逼的IO多路复用)

    Python的socket高级应用(多进程,协程与异步) 一.多进程multiprocessing multiprocessing is a package that supports spawnin ...

  7. 进击的Python【第十章】:Python的socket高级应用(多进程,协程与异步)

    Python的socket高级应用(多进程,协程与异步)

  8. day-5 python协程与I/O编程深入浅出

    基于python编程语言环境,重新学习了一遍操作系统IO编程基本知识,同时也学习了什么是协程,通过实际编程,了解进程+协程的优势. 一.python协程编程实现 1.  什么是协程(以下内容来自维基百 ...

  9. Python-09-线程、进程、协程、异步IO

    0. 什么是线程(thread)? 线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元.一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆 ...

  10. python 自动化之路 day 10 协程、异步IO、队列、缓存

    本节内容 Gevent协程 Select\Poll\Epoll异步IO与事件驱动 RabbitMQ队列 Redis\Memcached缓存 Paramiko SSH Twsited网络框架 引子 到目 ...

随机推荐

  1. centos 运行springboot 项目

    jar文件发布: 准备工作: 发布在springboot项目中的pom.xml文件添加如下: <build> <plugins> <plugin> <grou ...

  2. springboot 2.1.6.RELEASE整合Swagger2

    一.引入依赖 1 <modelVersion>4.0.0</modelVersion> 2 <groupId>com.badcat</groupId> ...

  3. Laravel11 从0开发 Swoole-Reverb 扩展包(二) - Pusher 协议介绍

    Pusher 协议概述 Pusher 协议 是一种用于实时 Web 通信的协议,它基于 WebSocket 技术,并提供了一套 发布-订阅(Pub/Sub)模式,用于让客户端(如浏览器.移动端.后端服 ...

  4. 使用Win32控制台实现libevent通信

    libevent版本:libevent-2.0.22-stable 服务端: #include <string.h> #include <errno.h> #include & ...

  5. Content-Encoding:br 是一种什么编码格式?

    一.前言 在之前测试HTTP应答的压缩过程中无意间发现在Google浏览器下出现了 Content-Encoding:br 这种的编码格式,当时我就纳闷了,前面不是一直在研究GZip压缩吗?br压缩又 ...

  6. AR 智能生态鱼缸组态远控平台 | 图扑软件

    在工业 4.0 和物联网技术的推动下,万物互联正重塑行业管理模式.组态远控系统作为高效管控的核心,打破了设备孤立状态,实现数据实时交互.以智能生态鱼缸为例,图扑软件低代码数字孪生平台通过集成前沿技术, ...

  7. java学习-8【EnumMap】

    EnumMap和EnumSet几乎是一样的,区别时EnumMap的key时Enum. public enum Types { RED,GREEN,BLACK,YELLO } @Test public ...

  8. 如何确定dbgrid选择的是记录而不是分组

    with cxgrdbtblvwGrid1DBTableView1.Controller do if FocusedRecord is TcxGridDataRow then begin i := c ...

  9. 配置Thymeleaf模板引擎

    1).thymeleaf-starter: 关闭缓存 2).静态资源都放在static文件夹下就可以按照路径直接访问 3).页面放在templates下,直接访问 springboot ,访问项目的时 ...

  10. windows下jdk版本切换(bat)

    1.jdk下载 Oracle官网 https://www.oracle.com/cn/ 资源->下载->Java下载 jdk当前最新版本 jdk22版本 jdk8版本 当前页面向下拉 2. ...