文章目录
- [第一章 C++20核心语法特性](#第一章 C++20核心语法特性)
-
- [1.3 协程 (Coroutines)](#1.3 协程 (Coroutines))
-
- [1.3.1 协程实现原理](#1.3.1 协程实现原理)
- [1.3.2 协程举例](#1.3.2 协程举例)
- [1.3.3 关键字解析](#1.3.3 关键字解析)
- [1.3.4 线程和协程的使用场景](#1.3.4 线程和协程的使用场景)
- [1.3.5 协程总结](#1.3.5 协程总结)
本文记录C++20新特性之协程(Coroutines)。
第一章 C++20核心语法特性
1.3 协程 (Coroutines)
为了提供异步代码的高效性,C++20引入了协程,它为异步编程提供了一种全新的范式。在C++20之前,处理异步操作(如网络IO),通常需要注册回调函数来处理数据。但是回调函数再次调用回调函数,甚至多次调用回调函数,就出现了"回调地狱",逻辑被分在了不同的回调函数中,导致逻辑分散。下面是一个 回调函数的例子:
cpp
class Reader
{
public:
void read(const std::function<void(int)> & callback)
{
std::thread t([callback]() {
this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
// 接收到33 数据
int value = 33;
callback(value); // 调用回调函数
});
t.join();
}
};
// 回调函数
void printValue(int value)
{
cout << "Received value: " << value << endl;
}
void test()
{
Reader reader;
reader.read(printValue);
// Received value: 33
}
但是,如果回调函数层次增加,回调函数又调用了其他函数,这就是"回调地狱"下面代码中printValue又调用了其他函数,用来处理这个value值。
cpp
class Reader
{
public:
void read(const std::function<void(int)> & callback)
{
std::thread t([callback]() {
this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
// 接收到33 数据
int value = 33;
callback(value); // 调用回调函数
});
t.join();
}
};
void handleValue(int value)
{
cout << "Handled value: " << value << endl;
}
// 回调函数
void printValue(int value)
{
handleValue(value);
//cout << "Received value: " << value << endl;
}
void test()
{
Reader reader;
reader.read(printValue);
// Received value: 33
}
这个处理逻辑被到多个函数中实现,数据流难以追踪,异常处理比较困难。C++20中引入了协程,旨在结合"同步代码的可读性"与"异步代码的高效性"。 它允许函数在执行过程中挂起 (Suspend),并在稍后恢复 (Resume),而无需销毁栈帧或阻塞线程。
1.3.1 协程实现原理
协程就是一个不依赖于任何线程栈的函数,协程切换时,"协程帧"(局部变量,参数等)保存在堆上,任何线程都可以直接访问。
当在函数中调用 co_wait,co_yield或co_return时,这个函数就是一个协程。
C++协程的三个概念
1 Promise对象(promise_type) : 这是协程的控制中心,规定了协程启动时做什么、结束时做什么、遇到异常如何处理、以及如何处理返回值。
2 Awaitable (可等待对象):这是 co_await 后面跟着的对象。它决定了协程是否真的要挂起,以及挂起后该做什么(例如注册一个回调,等 I/O 完成后恢复协程)。这个可等待对象必须实现 await_ready,await_suspend,await_resume三个接口。
3 Coroutine Handle (std::coroutine_handle):这是一个轻量级指针,用来控制协程的恢复和销毁。
1.3.2 协程举例
下面实现一个协程,并分析程序的调用过程。
cpp
// 实现一个 生成器,
struct Generator
{
// 声明 promise_type,这是编译器查找的约定名称
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
// 用于在协程外部控制协程的执行和生命周期
handle_type h;// 协程句柄
// 构造函数与析构函数
Generator(handle_type h_)
: h(h_)
{
}
~Generator() // RAII 销毁协程
{
if (h)
{
h.destroy();
}
}
// 只能移动,不能拷贝
Generator(const Generator&) = delete;
Generator& operator=(const Generator&) = delete;
Generator(Generator && obj) noexcept
: h(obj.h)
{
obj.h = nullptr;
}
// 恢复 协程执行一次
bool next()
{
h.resume();
return !h.done();
}
// 获取当前值
int getValue()
{
return h.promise().current_value;
}
// 2 定义 promise_type
struct promise_type
{
int current_value;
// 携程创建时 返回对象
Generator get_return_object()
{
return Generator{ handle_type::from_promise(*this) };
}
// 协程启动时的行为:suspend_always 表示启动后立即挂起,等待手动调用 next
std::suspend_always initial_suspend() { return {}; }
// 协程结束时的行为
std::suspend_always final_suspend() noexcept { return {}; }
// 处理 co_yield 表达式
std::suspend_always yield_value(int value) {
current_value = value;
return {}; // 挂起协程,将控制权交回调用者
}
// 处理 co_return
void return_void() {}
// 处理异常
void unhandled_exception() {
std::terminate();
}
};
};
// 创建协程
Generator sequence(int start, int step)
{
int i = start;
while (true)
{
// 挂起执行,并返回 i。下次 resume 时从这里继续。
co_yield i;
i += step;
if (i > 20)
{
co_return; // 结束协程
}
}
}
void test()
{
// 创建协程,此时处于挂起状态
auto gen = sequence(0, 5);
std::cout << "Start generator" << std::endl;
// 手动驱动
while (gen.next())
{
std::cout << "Generated value: " << gen.getValue() << std::endl;
}
std::cout << "Generator finished." << std::endl;
/*
Start generator
Generated value: 0
Generated value: 5
Generated value: 10
Generated value: 15
Generated value: 20
Generator finished.
*/
}
分析上面代码的执行流程:
1 调用 Generator gen = sequence(0, 5); 时,编译器发现sequence内部有 co_yield,所以将sequence 函数看作一个协程。
2 创建协程状态:编译器在堆上分配一个 "协程帧",并将,promise_type(协程的控制器,用于定义协程的行为 )局部变量i, step等保存在协程帧中。
3 创建返回对象:调用get_return_object() 并返回一个Generator对象,这个对象中持有这个协程帧的句柄。
4 协程开始执行。当协程开始执行时,遇到第一件事就是挂起,因为std::suspend_always ,所以协程在执行 while之前,先挂起。
5 返回gen : sequence 函数调用返回,gen变量现在持有一个已经创建但处于初始挂起状态的协程。
此时, 一个协程初始化完毕。然后,当执行到 while (gen.next()) 时,在next()内部调用了resume(),唤醒协程,协程开始执行。
第一次执行到while ()循环中,
i = 0, 执行到 co_yield i后,会调用 yield_value(0) 函数,并将该函数中的current_value 设置为0,因为yield_value 返回 std::suspend_always,协程再次被挂起。
控制权再次交给main线程,在主线程继续手动驱动 gen.next(),检查done(),因为协程没有结束,只是挂起,因此返回false,继续执行gen.next()后,返回true. 主线程继续执行,进入while循环体中,current_value为0,打印Generated value = 0.
第二次,继续执行gen.next(),再次唤醒协程,这个过程一直重复,直到i=20后,执行i += step变为25后,执行 co_return,协程开始结束。结束时,先调用 h.promise. return_void(),执行 final_suspend(),协程再次挂起,但这次的协程是完成状态。
所以,当再次调用next()时,协程已经完整了,next()返回false,跳出while(),结束。
总结执行过程:while (gen.next()) 循环就像一个手动泵。每一次 next() 调用就是"泵"一下,驱动协程执行一小段(从一个挂起点到下一个挂起点),并让它产生一个值。当协程通过 co_return 正常结束后,next() 会返回 false,循环自然停止。
1.3.3 关键字解析
co_await : 尝试挂起当前协程;
co_yield : 挂起协程并向调用者传出一个值。
co_return :结束协程执行,返回一个最终值。
1.3.4 线程和协程的使用场景
**使用线程:**当需要进行大规模的、可并行的科学计算、数据处理、图像渲染等 CPU 密集型任务时,通过多线程利用多核 CPU 的并行处理能力。
**使用协程:**当需要处理大量的网络请求、文件读写、数据库访问等 I/O 密集型任务时。使用协程,一个线程就可以管理成千上万个并发连接,因为在等待 I/O 完成时,线程不会被阻塞,而是可以切换去执行另一个准备就绪的协程。
1.3.5 协程总结
**协程优势:**协程类似函数,但是没有栈,状态保存在堆中,内存开销极小,通常只有几十字节的堆内存,上下文切换比操作系统的线程快得多(纳秒级)。