协程是 C++20 标准才正式加入的特性,C++17 通过 std::experimental::coroutine 预先实现了协程的框架。虽然在 C++17 里协程还属于实验阶段,但它已经展示了如何用更直观的方式写异步代码,减少回调地狱、提升代码可读性与维护性。本文将从协程的基本概念、关键字与语法、典型使用场景以及如何在 C++17 项目中引入协程进行阐述。
一、协程的基本概念
协程(coroutine)是一种轻量级的用户级线程,它可以在函数执行中随时挂起(yield)并恢复(resume),而不是像线程那样频繁地被操作系统调度。协程的优势体现在:
- 挂起与恢复:协程可以在任意位置挂起,随后从同一点继续执行。
- 状态保留:协程挂起时保留局部变量状态,恢复时无需重新创建栈。
- 更好的性能:相比多线程,协程切换更快,开销更小。
- 编写顺序化代码:可以像同步代码一样编写异步逻辑,消除回调链。
二、C++17 协程的实现框架
C++17 并未正式支持协程,但 std::experimental::coroutine 提供了实现原型。主要包含以下组件:
| 组件 | 作用 |
|---|---|
std::experimental::coroutine_handle |
控制协程的句柄,负责挂起与恢复 |
std::experimental::suspend_always / suspend_never |
表示挂起行为的协程悬停点 |
std::experimental::coroutine_traits |
为协程函数指定返回类型与 promise 类型 |
promise_type |
协程内部状态管理对象,负责协程的创建、销毁以及异常传播 |
下面给出一个最小可行的协程示例,演示如何在 C++17 环境下使用 std::experimental。
#include <experimental/coroutine>
#include <iostream>
#include <string>
struct Task {
struct promise_type;
using handle_type = std::experimental::coroutine_handle <promise_type>;
struct promise_type {
Task get_return_object() { return {handle_type::from_promise(*this)}; }
std::experimental::suspend_always initial_suspend() { return {}; }
std::experimental::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
handle_type coro;
Task(handle_type h) : coro(h) {}
~Task() { coro.destroy(); }
};
Task asyncPrint(const std::string& msg) {
std::cout << msg << std::endl;
co_return;
}
int main() {
auto t = asyncPrint("Hello, coroutine!");
t.coro.resume(); // 开始执行
}
上述代码演示了如何定义 Task 协程类型、提供 promise_type 并在协程内部使用 co_return。值得注意的是:
initial_suspend与final_suspend可以控制协程是否立即挂起或在完成后挂起。co_return与普通函数的return语义相同。
三、典型使用场景
1. 异步 I/O
在网络编程中,协程可以让 I/O 操作像同步代码一样写,避免回调链。示例伪代码:
Task asyncRead(Socket& s) {
std::string data;
while (true) {
co_await s.asyncReadInto(data); // 该函数返回一个 awaitable 对象
process(data);
}
}
2. 生成器(Generator)
协程天然适合作为生成器,支持 co_yield。例如:
struct IntGenerator {
struct promise_type {
int value_;
IntGenerator get_return_object() { return {handle_type::from_promise(*this)}; }
std::experimental::suspend_always initial_suspend() { return {}; }
std::experimental::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
std::experimental::suspend_always yield_value(int v) {
value_ = v; return {};
}
};
using handle_type = std::experimental::coroutine_handle <promise_type>;
handle_type coro;
};
IntGenerator range(int start, int end) {
for (int i = start; i <= end; ++i) co_yield i;
}
使用时:
auto gen = range(1, 5);
while (!gen.coro.done()) {
std::cout << gen.coro.promise().value_ << ' ';
gen.coro.resume();
}
3. 状态机
协程内部的挂起点可以用来实现复杂状态机,而无需显式维护状态变量。
四、在 C++17 项目中引入协程的实战技巧
- 使用标准库实验扩展
#include <experimental/coroutine>并开启实验编译标志(如-std=c++17 -fcoroutines或-std=c++20)。 - 封装 awaitable
为 I/O 或计时器等提供await_ready,await_suspend,await_resume接口,便于使用co_await。 - 异常安全
在promise_type的unhandled_exception中统一处理异常,避免协程泄露资源。 - 内存管理
协程句柄使用destroy()手动销毁,避免堆栈泄露;也可以使用std::shared_ptr包装协程句柄,形成引用计数。
五、总结
尽管 C++17 只提供了实验性的协程实现,但它为 C++ 开发者提供了一条探索更高效、可读的异步编程路径。通过 std::experimental::coroutine,我们可以在 C++17 项目中尝试协程的使用,提前为迁移到 C++20 做好准备。协程让异步代码保持同步写法,显著降低了回调地狱,提高了代码可维护性;同时,协程的轻量级切换也为高并发应用带来了更好的性能表现。未来随着标准化,协程将成为 C++ 生态中不可或缺的一部分。