Boost.Asio学习(5):c++的协程
协程是什么?
协程就是可以“暂停”和“继续”的函数,像在函数里打个断点,然后以后可以从断点继续运行,而不是重新开始。
线程 vs 协程:类比
想象你在写小说:
-
线程:
-
你开了 3 个作者(线程),每个人都在写书。
-
系统(OS)来回切换谁在写。
-
切换的时候要收拾桌子、换椅子、拿走稿纸(上下文切换),开销大。
-
-
协程:
-
你只有 1 个作者,但可以写一会儿暂停,去做别的,再回来接着写。
-
暂停的时候,只需在稿纸上插个书签(挂起点),下次从书签继续,开销极小。
-
| 特性 | 线程 | 协程 |
|---|---|---|
| 切换方式 | 抢占式(OS决定) | 协作式(程序决定) |
| 切换成本 | 高(要保存寄存器、堆栈) | 低(只是保存协程状态) |
| 并发 | 多线程可以真正并行 | 协程必须在同一个线程轮流跑 |
| 控制权 | OS 控制 | 程序员控制(通过 co_await) |
-
协程 ≠ 线程,它只是单线程内的“任务切换”机制。
-
协程可以跑在一个线程上,也可以结合线程池,让多个线程各自跑多个协程 → 混合使用。
-
协程不提供并行(不会用多个 CPU 核心),但可以轻松实现异步(不会阻塞线程)。
协程解决了什么问题?
传统异步写法(回调):
async_read(socket, buf, [](auto ec, auto size) {
async_write(socket, buf, [](auto ec, auto size) {
// 回调地狱
});
});
协程写法:
co_await async_read(socket, buf);
co_await async_write(socket, buf);
为什么开销小?
-
线程切换:需要保存 CPU 寄存器、切换内核态、切换栈,可能上百纳秒甚至微秒级。
-
协程切换:只保存协程状态(程序计数器 + 局部变量),在用户态完成,纳秒级。
使用
-
C++20 正式引入协程机制,底层通过 编译器转换 + 栈帧对象(promise) 实现。
-
三个关键字:
-
co_await—— 挂起并等待某个任务。 -
co_yield—— 挂起并返回一个值(类似生成器)。 -
co_return—— 返回最终值并结束协程。
-
协程的运行方式
协程不是线程,也不是普通函数,而是 编译器把协程函数转换成状态机,并返回一个“协程句柄”。
流程:
-
编译器检测函数是否包含
co_await/co_yield/co_return。 -
如果包含,编译器会生成:
-
协程状态对象(包含局部变量、挂起点、promise)。
-
协程句柄(
std::coroutine_handle<>)控制 resume/destroy。
-
-
第一次调用协程函数:
-
返回一个
awaitable对象,不会立刻执行全部逻辑。
-
-
调用
resume():-
协程运行到
co_await挂起。
-
-
再次
resume():-
从上次挂起点继续。
-
| 关键字 | 作用 |
|---|---|
co_return |
返回最终结果并结束协程 |
co_yield |
产出一个值并挂起(生成器) |
co_await |
挂起并等待某个操作完成 |
常见协程返回类型
-
std::generator<T>(C++23 提供) -
自定义
task<T> -
第三方库:
-
cppcoro(最常用)
-
Boost.Coroutine
-
协程常见写法
(1)最简单的生成器(C++23)
#include <coroutine>
#include <iostream>
#include <generator>
std::generator<int> numbers() {
for (int i = 0; i < 5; ++i)
co_yield i;
}
int main() {
for (auto v : numbers())
std::cout << v << "\n";
}
(2)异步任务(自定义 Task)
#include <coroutine>
#include <iostream>
// 1. 定义返回类型 Task
struct Task {
// 2. promise_type:编译器需要它来生成协程对象
struct promise_type {
// 编译器调用:创建协程返回值
Task get_return_object() {
return Task{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
// 协程开始前是否挂起?
std::suspend_never initial_suspend() noexcept { return {}; }
// 协程结束后是否挂起?
std::suspend_never final_suspend() noexcept { return {}; }
// 处理 co_return
void return_void() {}
void unhandled_exception() {} // 异常处理
};
std::coroutine_handle<promise_type> handle;
Task(std::coroutine_handle<promise_type> h): handle(h) {}
~Task() { if(handle) handle.destroy(); }
};
// 3. 协程函数:编译器自动用 Task::promise_type 来管理
Task myCoroutine() {
std::cout << "Hello\n";
co_await std::suspend_always{}; // 挂起协程
std::cout << "World\n";
}
int main() {
auto t = myCoroutine(); // 创建协程,执行到 co_await 挂起
std::cout << "Resume...\n";
t.handle.resume(); // 恢复协程,继续执行
}
自定义 Task详细解析
为什么要定义 promise_type
-
C++20 协程和普通函数完全不同,编译器在看到协程时不会直接返回值,而是生成一个状态机对象(Frame)。
-
协程的返回类型必须定义一个 嵌套类型
promise_type。 -
编译器规则:
-
当你写:
-
Task myCoroutine() {
co_await something;
co_return;
}
编译器会查找 Task::promise_type,并生成代码去:
-
创建一个
promise_type对象。 -
调用它的
get_return_object(),把结果作为协程的返回值。 -
调用
initial_suspend()/final_suspend()控制是否立即挂起。 -
调用
return_void()或return_value()处理co_return。
所以:
-
promise_type就是协程的“大脑”,控制协程生命周期。 -
它必须提供一组固定接口(编译器调用)。
initial_suspend和final_suspend
initial_suspend():协程刚创建时是否挂起?-
返回
std::suspend_always→ 挂起,调用者必须手动resume()。 -
返回
std::suspend_never→ 不挂起,协程立即执行。
-
final_suspend():协程结束后是否挂起?-
一般返回
std::suspend_always,让调用者决定何时销毁协程。
-
-
为什么要控制挂起?
-
这关系到执行模型(立即运行 or 手动恢复)。
-
std::coroutine_handle是干啥的
-
它是一个 轻量级句柄,指向协程帧(frame)。
-
协程帧包含:
-
状态机(状态编号)
-
局部变量
-
一个
promise_type对象(由返回类型的promise_type定义)
-
-
std::coroutine_handle提供操作协程生命周期的方法:-
恢复协程(
resume()) -
销毁协程(
destroy()) -
判断是否执行完(
done())
-
本质:它类似一个 void*,但知道协程帧布局,可以安全操作 promise 对象。
重点:协程不是线程,编译器不会自动跑完,你必须显式调用 resume(),所以要有句柄。
为什么模板参数是 promise_type?
-
协程帧里有一个
promise_type成员。 -
coroutine_handle<promise_type>是一个类型安全的 handle,可以直接访问promise对象:
handle.promise() // 返回对 promise 对象的引用
这就是为什么你的 Task 返回类型必须定义 promise_type,编译器才能生成 coroutine_handle<promise_type>。
如果不关心 promise,可以用 无模板版本:
std::coroutine_handle<> // 泛型 handle,不提供 promise()。
类成员函数
1. resume()
-
恢复协程执行。
-
如果协程在挂起状态,继续执行,直到下一个挂起点或结束。
-
所以resume()的同步地、阻塞的
2. done()
-
判断协程是否执行完毕(到达
final_suspend()之后)。done()== true 表示协程执行到final_suspend()。
3. destroy()
-
销毁协程帧(释放内存),必须在协程完全结束后调用,否则 UB。
4. promise()
-
返回
promise_type&,可以访问自定义逻辑,比如取值、状态。
举例按流程详细解释
代码流程:
#include <coroutine>
#include <iostream>
// 1. 协程返回类型 Task
struct Task {
struct promise_type {
// 编译器会调用此函数获取协程返回对象
Task get_return_object() {
std::cout << "[promise_type] get_return_object()\n";
return Task{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() noexcept {
std::cout << "[promise_type] initial_suspend()\n";
return {}; // 创建后立即挂起
}
std::suspend_always final_suspend() noexcept {
std::cout << "[promise_type] final_suspend()\n";
return {}; // 协程结束后挂起,等外部销毁
}
void return_void() { std::cout << "[promise_type] return_void()\n"; }
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
explicit Task(std::coroutine_handle<promise_type> h): handle(h) {}
~Task() {
if (handle) {
std::cout << "[Task] destroy coroutine\n";
handle.destroy(); // 销毁协程 frame
}
}
void resume() {
std::cout << "[Task] resume()\n";
handle.resume(); // 恢复协程
}
};
// 2. 协程函数:执行顺序受 suspend 控制
Task myCoroutine() {
std::cout << "[Coroutine] Start\n";
co_await std::suspend_always{}; // 挂起
std::cout << "[Coroutine] After co_await\n";
co_return;
}
int main() {
auto t = myCoroutine(); // 创建协程(不会立刻跑完整个函数)
std::cout << "[main] first resume\n";
t.resume(); // 执行到 co_await 挂起
std::cout << "[main] second resume\n";
t.resume(); // 执行到 co_return,final_suspend 挂起
}
输出:
[promise_type] get_return_object()
[promise_type] initial_suspend()
[main] first resume
[Task] resume()
[Coroutine] Start
[Coroutine] After co_await
[promise_type] return_void()
[promise_type] final_suspend()
[main] second resume
[Task] destroy coroutine
执行过程详细解读:
Step 1: 调用 myCoroutine()
-
编译器看到
co_await→ 把myCoroutine改造成协程,不直接返回Task,而是:-
编译器硬编码规则:
-
如果协程返回类型是
R,就去找R::promise_type。 -
通过
R::promise_type生成一个promise_type类的对象promise。
-
-
调用
get_return_object()→ 返回Task,并绑定coroutine_handle。-
std::coroutine_handle<promise_type>::from_promise(*this):把 promise 和 handle 关联。
-
-
调用
initial_suspend()→ 返回suspend_always,协程挂起,不执行函数体。
-
-
此时:协程 frame(状态机)在堆上创建,但没开始执行。
对象状态:
-
Task:持有coroutine_handle。 -
promise_type:存储协程状态。 -
handle:指向协程 frame,能控制resume()。
Step 2: t.resume()
-
调用
handle.resume()。 -
协程开始执行,打印
[Coroutine] Start。 -
遇到
co_await std::suspend_always{}→ 立即挂起。-
编译器调用
await_suspend()(内部由suspend_always实现)。
-
-
控制权返回
main,协程停在co_await这一行。
Step 3: 第二次 t.resume()
-
协程从
co_await后恢复。 -
打印
[Coroutine] After co_await。 -
执行
co_return→ 编译器调用promise_type.return_void()。 -
协程执行到尾,进入
final_suspend()→ 返回suspend_always,协程再次挂起,等待销毁。
Step 4: 退出 main(),Task 析构
-
调用
handle.destroy()→ 销毁协程 frame 和promise_type。 -
协程彻底结束。
关键点汇总
-
promise_type是协程的“控制器”,编译器强制要求它有:-
get_return_object()(返回 Task) -
initial_suspend()/final_suspend() -
return_void()/return_value()
-
-
coroutine_handle是“遥控器”,用来resume()和destroy()。 -
co_await触发挂起点。 -
co_return结束协程。 -
状态机 + Frame 在堆上管理生命周期。
co_yield使用详细解释
编译器在看到 co_yield expr; 时,会将它转换成 对 promise_type.yield_value(expr) 的调用,并把返回值当作 co_await 的对象。
也就是说:
co_yield value;
等价于:
co_await promise.yield_value(value);
所以:
-
co_yield会调用promise_type::yield_value(T value)。 -
这个函数必须返回一个 Awaitable 对象(通常是
std::suspend_always或std::suspend_never)。 -
编译器会像
co_await一样调用:-
await_ready()→ 决定是否挂起。 -
await_suspend()→ 挂起时的动作。 -
await_resume()→ 恢复后取值。
-
举个完整例子:生成器
#include <coroutine>
#include <iostream>
struct Generator {
struct promise_type {
int current_value;
Generator get_return_object() {
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
// **核心:co_yield 调用这个**
std::suspend_always yield_value(int value) noexcept {
std::cout << "[promise_type] yield_value(" << value << ")\n";
current_value = value;
return {}; // 挂起
}
};
std::coroutine_handle<promise_type> handle;
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
bool move_next() {
if (!handle.done()) {
handle.resume();
}
return !handle.done();
}
int current_value() { return handle.promise().current_value; }
};
// 协程函数
Generator numbers() {
for (int i = 1; i <= 3; ++i) {
co_yield i; // 调用 yield_value(i)
}
}
int main() {
auto gen = numbers();
while (gen.move_next()) {
std::cout << "[main] got: " << gen.current_value() << "\n";
}
}
执行流程
[promise_type] yield_value(1)
[main] got: 1
[promise_type] yield_value(2)
[main] got: 2
[promise_type] yield_value(3)
[main] got: 3
执行到 co_yield 时发生了什么?
-
编译器调用
promise_type.yield_value(i)。 -
这个函数保存值(
current_value = i),返回std::suspend_always。 -
协程挂起(
await_suspend())。 -
控制权返回
main(),用户取值。 -
下一次
resume(),协程从co_yield之后继续。
return_value用法
co_return 在 C++20 协程中不仅可以 co_return;,还可以 co_return value;,对应 promise_type 的 return_value() 方法(或 return_void())。
-
co_return用于在协程函数里 返回结果 或 结束协程。 -
两种形式:
-
co_return;:无返回值(void协程)。 -
co_return expr;:返回一个值。
-
编译器会把 co_return 翻译成 调用 promise 对象的方法:
-
co_return;→promise.return_void(); -
co_return expr;→promise.return_value(expr);
例子:
#include <coroutine>
#include <iostream>
#include <optional>
struct Task {
struct promise_type {
std::optional<int> value;
Task get_return_object() {
return Task{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(int v) { value = v; } // 支持 co_return v;
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> h;
explicit Task(std::coroutine_handle<promise_type> h) : h(h) {}
~Task() { if (h) h.destroy(); }
int get() {
h.resume();
return *h.promise().value;
}
};
Task compute() {
std::cout << "Computing...\n";
co_return 42; // 等价于 promise.return_value(42);
}
int main() {
Task t = compute();
std::cout << "Result = " << t.get() << "\n";
}
协程函数不会像普通函数那样直接“返回值”给调用者,而是:
-
立即返回一个包装了
coroutine_handle的对象(例如Task), -
真正的“结果”要么通过:
-
promise_type存储(由co_return设置值), -
或由外部代码通过
handle或包装类(如Task::get())访问。
-
这个std::optional<T>又是个啥?
std::optional<T> 是 C++17 引入的一个标准库模板类,用来表示 “一个可能有值,也可能没有值的对象”。
在协程里,它常用来延迟存储返回值,因为:
-
协程一开始不会立即有返回值(因为可能挂起)。
-
但最终会
co_return value;,所以我们需要一个容器,能先处于“空”状态,等有值再存进去。
std::optional<T> 基本概念
-
它是一个模板类,可以包含类型
T的值,或者为空。 -
类似于
T*(指针),但更安全,因为不会出现悬空指针,也不需要动态分配。 -
常用接口:
std::optional<int> opt; // 默认无值
opt.has_value(); // 判断是否有值
opt = 42; // 赋值
int x = *opt; // 取值(必须有值,否则未定义)
为什么用 optional 而不是直接 int?
-
因为协程返回
Task<int>,在协程挂起时我们没有值。 -
如果直接用
int value;,我们没法区分“值是 0”还是“值还没准备好”。 -
std::optional<int>可以:-
初始状态
nullopt(空)。 -
return_value(42)时赋值。 -
之后通过
*optional或optional.value()取值。
-
更多推荐
所有评论(0)