C++20 Coroutine

First Post:

Last Update:

Word Count:
2.9k

Read Time:
12 min

C++20 Coroutine

协程是什么?

协程(Coroutines)是一种计算机程序组件,它允许在一个函数中暂停执行,并在稍后恢复执行。简而言之,协程是一种可以在函数执行期间暂停和恢复的控制流。

协程函数与普通函数有什么区别?

传统的函数在调用时开始执行,然后在返回时结束执行。而协程允许函数在执行过程中挂起,并在需要时恢复执行,而不是一次性执行完毕。

解决什么问题?

协程在编写异步代码时非常有用,因为它们可以使异步代码更加清晰、简洁,避免了回调地狱(Callback Hell)的问题。通过协程,可以以顺序的方式编写异步代码,而不必依赖于繁琐的回调函数或者复杂的线程同步机制。

碎语

协程在许多编程语言中都有支持,例如C++20引入了对协程的原生支持,Python的async/await语法也是基于协程的概念实现的。在异步编程中,协程已经成为一种重要的编程范式。然而才在C++20引入了协程标准,可以不用依赖于其他协程库就实现程序在执行过程中暂停和恢复,而不会阻塞整个线程。有了协程使得编写异步代码更加简洁和易读。而官方和网上的案例相对比较局限,看着也不是那么通俗易懂,大部分案例也是嵌入到其他框架一起介绍的,这篇文章就以简单的几个小例子,更好的理解c++20的协程使用方法。

直接看母语

话不多说,我们直接先上一段封装好的小代码,先不管下面这个代码是啥,我们来研究如何使用就可以,后文再做详细的介绍。

下面代码命名保存为 coroutine.h,后面我们的案例中都包含该头文件。该头文件现已用在squick框架中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#pragma once
#include <coroutine>
#include <functional>
#include <time.h>

template<typename T>
class Coroutine {
public:
struct promise_type {
T value_;
auto initial_suspend() noexcept { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
void unhandled_exception() noexcept {}
Coroutine get_return_object() { return Coroutine{ handle_type::from_promise(*this) }; }
void return_void() {}
template<std::convertible_to<T> From>
std::suspend_always yield_value(From&& from) {
value_ = std::forward<From>(from);
return {};
}
};
using handle_type = std::coroutine_handle<promise_type>;
explicit Coroutine(handle_type handle) : coro_handle_(handle) { start_time_ = time(nullptr); }
~Coroutine() {}
handle_type GetHandle() const {
return coro_handle_;
}
time_t GetStartTime(){
return start_time_;
}
time_t start_time_ = 0;
private:
handle_type coro_handle_;
};

template<typename T>
class Awaitable {
public:
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
coro_handle_ = h;
handler_.operator()(this);
}
T await_resume() { return data_; }
T data_;
std::function< void(Awaitable<T>* awaitable)> handler_;
std::coroutine_handle<> coro_handle_;
};

例子1-创建协程

保存为demo1.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "coroutine.h"
#include <iostream>
Coroutine<int> MyCoroFunc() {
std::cout << "hello coroutine!\n";
co_return;
}

int main() {
auto co = MyCoroFunc(); // 创建协程
auto h = co.GetHandle();
std::cout << "Coroutine address: " << h.address() << std::endl;
h.resume(); // 运行协程
if(h.done()) {
h.destroy(); // 销毁协程
std::cout << "Coroutine destroyed\n";
}
return 0;
}

编译

1
g++ demo1.cc --std=c++20

输出

1
2
3
Coroutine address: 0x55c4d0609eb0
hello coroutine!
Coroutine destroyed

通过该例子我们已经创建了个协程,并且去调用了MyCoroFunc函数。

例子2-获取协程函数中的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "coroutine.h"
#include <iostream>

Coroutine<int> MyCoroFunc() {
std::cout << "MyCoroFunc: Coroutine run\n";
co_yield 5;
std::cout << "MyCoroFunc: Coroutine continue \n";
co_return;
}

int main() {
auto co = MyCoroFunc(); // 创建协程
auto h = co.GetHandle();
std::cout << "Coroutine is created, address: " << h.address() << std::endl;
h.resume(); // 运行协程, 执行到co_yield返回

std::cout << "MyCoroFunc co_yield value: " << h.promise().value_ << std::endl;
h.resume(); // 继续运行协程

if(h.done()) {
h.destroy(); // 销毁协程
std::cout << "Coroutine destroyed\n";
}
return 0;
}

输出:

1
2
3
4
5
Coroutine is created, address: 0x56507223ceb0
MyCoroFunc: Coroutine run
MyCoroFunc co_yield value: 5
MyCoroFunc: Coroutine continue
Coroutine destroyed

例子3-如何在协程函数运行过程中异步获取外界传来的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include "coroutine.h"
#include <iostream>
#include <string>

class MyWork {
struct Data {
int id;
std::string msg;
};
public:
// 我们的Awaitable回调函数,主要用于知道是哪一个协程挂起了,截获协程的handler,用于后面进行恢复协程。
void MyAwaitbleFuncHandler(Awaitable<Data> *a) {
std::cout << "MyAwaitableFuncHandler, bind your corotine to manager\n";
this->awaitable_ = a;
}
// 我们自己的Awaitble函数,可以把它比作http的客户端向第三方服务器发起请求过程。
Awaitable<Data> MyAwaitbleFunc() {
Awaitable<Data> a;
a.data_.id = 1234;
a.handler_ = std::bind(&MyWork::MyAwaitbleFuncHandler, this, std::placeholders::_1);
return a;
}
// 协程函数
Coroutine<int> MyCoroFunc() {
std::cout << "MyCoroFunc: Coroutine run\n";
Data d = co_await MyAwaitbleFunc(); // 异步执行MyAwaitbleFunc函数。
std::cout << "MyCoroFunc: MyAwaitableFunc return: " << d.msg << "\n";
co_return;
}

void DoWork() {
auto co = MyCoroFunc(); // Create a corotine
auto h = co.GetHandle();
std::cout << "Coroutine is created, address: " << h.address() << std::endl;
h.resume(); // 运行协程
// 设置await的返回值
this->awaitable_->data_.msg = "Hello awaitble";

h.resume(); // 继续运行协程
if(h.done()) {
h.destroy();
std::cout << "Coroutine destroyed\n";
}

}
private:
Awaitable<Data> *awaitable_ = nullptr;
};

int main() {
MyWork w;
w.DoWork();
return 0;
}

输出

1
2
3
4
5
Coroutine is created, address: 0x55cb276c4eb0
MyCoroFunc: Coroutine run
MyAwaitableFuncHandler, bind your corotine to manager
MyCoroFunc: MyAwaitableFunc return: Hello awaitble
Coroutine destroyed

解释

看完了上面例子,看不懂也没关系,我在这里做一些简单的概念。

C++20引入了协程(Coroutines)的支持,这是一项重要的新功能,使得异步编程变得更加容易和直观。协程提供了一种在函数内部暂停和恢复执行的机制,从而使得编写异步代码更加简洁、可读性更高。

以下是C++20协程的一些关键概念和特性:

  1. 协程关键字: C++20引入了co_awaitco_yieldco_return等新的关键字,用于在协程内部进行挂起、恢复和返回操作。
  2. 协程函数: 使用co_return可以在协程函数中返回值,并在此处暂停协程的执行。协程函数可以返回一个期待值,而不是立即返回,从而使得在异步操作完成后再继续执行。
  3. 协程生成器(Coroutine Generator): 通过co_yield关键字,可以在协程内部产生值并暂停执行。这种机制非常适合生成序列或流式数据。
  4. 协程状态机: 编译器会将协程函数转换成状态机的形式,以便在暂停和恢复时保存和恢复执行上下文。
  5. std::coroutine_handle 这是一个轻量级的句柄,用于管理协程的生命周期和执行状态。它可以用来手动控制协程的执行,例如恢复、挂起和销毁。
  6. 协程的异步编程: 协程可以与异步任务一起使用,例如与Future、Promise、I/O操作等结合,从而实现高效的异步编程模式,而无需显式地使用回调函数或复杂的线程管理。
  7. 协程的异常处理: 协程内部的异常可以通过co_awaitco_yield传递给调用方进行处理,也可以通过协程的promise_type自定义异常处理逻辑。
  8. 协程库: C++20标准库提供了与协程相关的头文件<coroutine>,其中包含了一些与协程相关的类和函数,例如std::coroutine_traitsstd::suspend_alwaysstd::suspend_never等。

来看看先前例子中所写的头文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#pragma once
#include <coroutine>
#include <functional>
#include <time.h>

template<typename T>
class Coroutine { // 该类主要用于创建协程,且确定协程采用什么机制运行,有promise_type结构体负责。
public:
// 协程状态管理,如果类中有promise_type且里面封装有协程的内置函数,编译器会自动给根据返回了该类的函数以及内嵌的函数来综合判定该函数是不是协程函数。
struct promise_type { // promise_type是一个关键的概念,它是用于控制协程行为的一种特殊的类型。
T value_; // 协程的返回值
auto initial_suspend() noexcept { return std::suspend_always{}; } // 创建协程时调用, 返回std::suspend_always代表创建时挂起,当然也可以让它直接运行,std::suspend_nerver
auto final_suspend() noexcept { return std::suspend_always{}; } // 协程结束时调用
void unhandled_exception() noexcept {} // 异常处理, 该函数来处理在协程内部抛出的异常。
Coroutine get_return_object() { return Coroutine{ handle_type::from_promise(*this) }; } // 返回对象管理,该函数,用于创建协程的返回对象,通常会返回一个Coroutine对象或者std::coroutine_handle对象,以便外部代码可以操作协程。
void return_void() {} // 可以允许void 返回,相当于允许 co_return 关键字
template<std::convertible_to<T> From>
std::suspend_always yield_value(From&& from) { // 可以使用 co_yield 关键字让协程挂起,外界可使用协程读取promise对象获取yield的值
value_ = std::forward<From>(from);
return {};
}
};
using handle_type = std::coroutine_handle<promise_type>; // 协程的handler,可以将该handler传递给外界管理协程,里面封装了几个常用的函数,如 address(), done(), resume, promise()
// address: 获取协程的地址
// done: 该协程是否执行完毕
// resume: 恢复协程,继续运行
// promise: 获取到promise_type对象,主要用于获取 co_yield传递的值。
explicit Coroutine(handle_type handle) : coro_handle_(handle) { start_time_ = time(nullptr); } // 创建协程时调用该构造函数
~Coroutine() {}
handle_type GetHandle() const { // 获取协程handle
return coro_handle_;
}
time_t GetStartTime(){
return start_time_;
}
time_t start_time_ = 0;
private:
handle_type coro_handle_;
};

template<typename T>
class Awaitable { // 该类主要用于协程函数中能让协程挂起,从外界传值到协程函数中去
public:
bool await_ready() { return false; } // 再调用Awaitable函数时最先运行,基本没啥作用
void await_suspend(std::coroutine_handle<> h) { // 再协程挂起后运行,我们再里面通过运行我们的回调函数将协程的handler传递出去,方便外界自行去管理该协程。在达到一定时期时,用于恢复协程。
coro_handle_ = h; //
handler_.operator()(this); // 调用我们的回调函数
}
T await_resume() { return data_; } // co_await 函数的返回值
T data_; // co_await 函数返回值
std::function< void(Awaitable<T>* awaitable)> handler_; // 回调函数,这我自己写的可以不用,只是方便在协程挂起时,能够通过回调函数捕获到是哪一个协程挂起了。
std::coroutine_handle<> coro_handle_; // 协程handler
};

就先介绍到这里了,学会了以上基本没啥大问题,协程的内存管理,可以通过智能指针来管理,以上例子中都是手动释放内存的。对协程的内存管理也是一门学问,看自己的框架怎么设计喽。比如下面这几个伪代码是利用上面我介绍的例子实际运用到http服务器中的。

https://github.com/pwnsky/squick/blob/main/src/tutorial/t5_http/http_module.cc

1
2
3
4
5
6
Coroutine<bool> HttpModule::ClientAsyncGet(std::shared_ptr<HttpRequest> req) {
std::cout << "You use await to async request another server\n";
auto data = co_await m_http_client_->CoGet("http://www.bilibili.com");
m_http_server_->ResponseMsg(req, data.content, WebStatus::WEB_OK);
co_return;
}

大家有兴趣可以去看看我写的squick游戏框架。

参考

https://en.cppreference.com/w/cpp/language/coroutines

https://chat.openai.com/chat

打赏点小钱
支付宝 | Alipay
微信 | WeChat