测试过的编译器如下

编译器版本 编译命令
gcc 11.2.0 g++ -std=c++20
clang 13.0.1 clang++ -std=c++20 -stdlib=libc++

2022年了,C++20终于趋于落地。除去还需要时间沉淀的Modules,我们有将近完美的Ranges,成熟的Concept,以及最特殊的Coroutine。

Coroutine与众不同,他对标准库/语言的改动非常小,但却是最重量级的——有许多编译器和第三方库跟进,享受着原生高性能协程以及三个新关键字co_await/co_yield/co_return带来的舒适的编程体验。之后的Executor和Network也需要Coroutine打下的底子,Executor的野心是高性能地抽象统一整个计算领域,而Network就不知道C++程序员们等待了多久了。Coroutine的重要性可见一斑。所以本文就来介绍一下Coroutine(协程)。

什么是协程

A coroutine is a special subprogram that has various entries

子程序就是协程的一种特例。——Donald Knuth

在C++20中,协程指的是一个能够暂停执行函数

书面的来说,是包含co_await/co_return/co_yield的函数。

能够暂停执行意味着这两件事

  1. 必须持久化保存函数运行时的执行上下文

我们都知道,一个正常的C++函数运行时所创建的自动存储期的变量都是开在栈上的,而这个栈的最大大小和各种寄存器大小和传入参数的大小编译期是可以知道的。所以在调用协程函数时编译器就可以在堆中分配出这个栈帧和用于保存寄存器、传入参数、调用方地址的一些空间,这里称为一个函数的执行上下文。在函数执行/恢复执行时调用方将寄存器保存在调用方栈上,调用方地址(PC,ESP)放入执行上下文,恢复执行上下文的寄存器/PC/ESP等,即可执行/恢复执行函数。函数暂停即根据执行上下文中的调用方信息恢复调用方调用时状态的过程。

编译器在调用方调用协程函数后马上执行这个过程,然后创建coroutine_handle类,存储能控制协程函数的句柄,这里称为协程的创建

  1. 调用方可以得到这个协程(函数)在堆中的句柄

既然堆中分配了这个协程的栈,那么调用方应该有权利管理协程的生命周期。协程函数在定义时的返回值的类型,这里设为future-type,本文介绍的一种实现就是调用方直接使用coroutine_handle管理协程。

  1. 除了这两点之外,应该也要给第三方库一个足够的定制空间,在各个阶段都留有能够让第三方库介入定制的行为。

C++20在不改变静态类型和RAII的框架下是怎么实现的呢?他们定义了这么一个鸭子类型promise-type(不是std::promise),通过coroutine_traits获取到future-type对应的promise-type类型。

template<class, class...>
struct coroutine_traits {};
 
template<class R, class... Args>
    requires requires { typename R::promise_type }
struct coroutine_traits<R, Args...> {
    using promise_type = R::promise_type;
};

然后将以下代码插入进函数

{
    /*
    协程的创建
    */
   promise-type promise promise-constructor-arguments ;
    /*
    尝试将函数参数传入到promise构造函数。如果失败则使用默认构造函数。
    */
   try {
      co_await promise.initial_suspend();
      <function-body>
   } catch ( ... ) {
      if (!initial-await-resume-called)
         throw ;
      promise.unhandled_exception() ;
   }
final-suspend :
   co_await promise.final_suspend() ;
}

如你所见,每一个协程函数都会有对应的promise-type变量,那么coroutine_handle就可以根据模板参数获取这个promise

而为了实现这一过程,promise-type类型必须要有下面名字的成员函数。

SOME_TYPE get_return_object();//用于得到协程函数开始执行的返回值
SOME_TYPE initial_suspend();//用于协程函数开始执行时执行暂停操作
void return_void();//协程函数内部co_return终止返回void时执行的操作(和下面return_value同时只能有一个存在)
void return_value(SOME_TYPE);//协程函数内部co_return终止返回非void值时执行的操作
SOME_TYPE final_suspend()noexcept;//用于协程函数结束执行时执行暂停操作
void unhandled_exception();//用于协程函数内部有未接住的异常时执行

一个最简单的协程

本程序创建了两个计算初始项为1,2的斐波那契数列并交错执行。

#include <iostream>
#include <coroutine>
#include <tuple>

using namespace std;

struct promise;//声明

namespace std{
    //指定coroutine_handle<promise>使用promise作为promise-type
	template <class ...Args>
	struct coroutine_traits<coroutine_handle<promise>,Args...>{
		using promise_type=promise;
	};
}

struct promise{
	promise(int& a){
        a++;
	}
	
    //协程刚开始执行不会暂停
	auto initial_suspend() { return suspend_never{}; }
    //协程结束执行时会马上暂停,然后清理
	auto final_suspend() noexcept { return suspend_always{}; }
	
	void unhandled_exception() {
		terminate();
	}
	
	coroutine_handle<promise> get_return_object() {
        //调用协程函数直接返回coroutine_handle
	    return coroutine_handle<promise>::from_promise(*this);
	}
	
	auto yield_value(int value) {
        //协程内部co_yield时就调用此函数
	    value_ = value;
	    return suspend_always{};
	}
    
	int value_;
    //保存上次运行协程的返回值
};


coroutine_handle<promise> fibonacci(int a=1){
    int i=1,j=a;
    while (1){
        tie(i,j)=make_pair(j,i+j);
        co_yield j;
    }
}

int main(int argc, char** argv) {
	auto x = fibonacci(),y=fibonacci();//创建两个协程
	for ( ;x.promise().value_ < 1000&&y.promise().value_ < 1000; ) {
		cout << "fibo: " << x.promise().value_ << " " << y.promise().value_ << endl;
		if (rand()%2) x();//交错继续
		else y();
	}
	x.destroy();y.destroy();//手动销毁协程
	return 0;
}