C++学习:六个月从基础到就业------C++20:协程(Coroutines)
本文是我C++学习之旅系列的第五十篇技术文章,也是第三阶段"现代C++特性"的第十二篇,继续介绍C++20引入的新特性,本篇重点是协程(Coroutines)。查看完整系列目录了解更多内容。
引言
在现代软件开发中,异步编程已经成为处理I/O操作、并发任务和事件驱动系统的关键范式。然而,传统的基于回调或基于Future/Promise的异步编程模型常常导致代码复杂、难以维护,甚至出现所谓的"回调地狱"。C++20引入的协程(Coroutines)提供了一种革命性的方法来简化异步编程,使开发者能够以看似同步的方式编写异步代码。
协程是一种可以暂停执行并在之后恢复的函数。与普通函数不同,协程可以在执行过程中让出控制权,并在适当的时机重新获得控制权继续执行。这种能力使得协程特别适合处理异步操作、生成器模式和其他需要控制流灵活转移的场景。
本文将介绍C++20协程的基本概念、工作原理、核心组件以及实际应用场景,帮助你掌握这一强大的新特性,编写更简洁、更高效的异步代码。
目录
协程基础概念
什么是协程
协程(Coroutine)是一种特殊的函数,能够在执行过程中暂停并保存当前状态,稍后再从暂停的位置继续执行。这种能力使协程成为处理异步操作的强大工具。
协程的核心特点:
- 可暂停执行:协程可以在指定点暂停执行,并让出控制权
- 状态保存:暂停时,协程的执行状态(包括局部变量和执行位置)被保存
- 可恢复执行:协程可以从上次暂停的位置恢复执行
- 多入口多出口:与传统函数的"单入口单出口"不同,协程可以有多个出口(暂停点)和入口(恢复点)
下面是一个简单的协程概念示例(使用伪代码):
协程 generate_sequence() {
yield 1; // 产生值1并暂停
yield 2; // 恢复后产生值2并再次暂停
yield 3; // 恢复后产生值3并再次暂停
return; // 结束协程
}
主函数() {
generator = generate_sequence();
value1 = generator.next(); // 获取1
value2 = generator.next(); // 获取2
value3 = generator.next(); // 获取3
}
协程与线程的区别
协程和线程都是实现并发的机制,但它们有根本性的区别:
特性 | 协程 | 线程 |
---|---|---|
调度方式 | 协作式调度(自己决定何时让出控制权) | 抢占式调度(由操作系统调度) |
切换开销 | 非常低(通常只涉及少量寄存器) | 较高(涉及完整的上下文切换) |
存储空间 | 共享同一线程的栈空间 | 每个线程有独立的栈空间 |
并行执行 | 同一时刻只有一个协程在执行(单线程内) | 可以真正并行执行(多核处理器上) |
同步机制 | 通常不需要复杂的同步原语 | 需要互斥锁、条件变量等同步机制 |
协程的主要优势:
- 更轻量级,创建和销毁成本低
- 切换开销小,适合频繁切换的场景
- 不需要考虑大多数并发问题,简化编程模型
- 特别适合I/O密集型应用,可大幅提高性能
C++20协程的特点
C++20引入的协程具有以下特点:
- 低级机制:C++20提供的是协程的底层基础设施,而非高级抽象
- 编译器支持:依赖编译器支持,将协程函数转换为状态机
- 可定制性:高度可定制,允许库开发者构建各种高级协程抽象
- 零开销抽象:设计目标是提供零开销抽象,不为不使用的特性付出代价
- 无栈协程:C++20使用的是无栈协程模型,协程状态存储在堆上而非独立栈上
C++20协程的关键语法元素:
co_await
:暂停协程执行,等待某个操作完成co_yield
:产生一个值并暂停执行co_return
:完成协程执行并返回一个值
一个协程在C++20中的标志是使用了以上任一关键字。
协程的语法与组件
co_await表达式
co_await
是C++20协程的核心操作,用于暂停协程执行,等待某个操作完成,然后恢复执行:
cpp
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
// 一个简单的可等待对象
struct SimpleAwaiter {
bool await_ready() const noexcept {
return false; // 表示需要暂停协程
}
void await_suspend(std::coroutine_handle<> handle) const noexcept {
// 模拟异步操作,2秒后恢复协程
std::thread([handle]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
handle.resume(); // 恢复协程执行
}).detach();
}
int await_resume() const noexcept {
return 42; // 返回给co_await表达式的结果
}
};
// 一个简单的协程返回类型
struct SimpleTask {
struct promise_type {
SimpleTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
// 使用co_await的简单协程
SimpleTask simple_coroutine() {
std::cout << "协程开始执行" << std::endl;
std::cout << "协程即将暂停..." << std::endl;
int result = co_await SimpleAwaiter{}; // 在这里暂停协程
std::cout << "协程恢复执行,收到结果: " << result << std::endl;
std::cout << "协程执行完毕" << std::endl;
}
co_await
表达式的处理流程:
- 调用等待对象的
await_ready()
方法 - 如果返回
true
,直接继续执行协程 - 如果返回
false
,暂停协程执行并调用等待对象的await_suspend(handle)
方法 - 当协程需要恢复时,调用协程句柄的
resume()
方法 - 协程恢复后,调用等待对象的
await_resume()
方法获取结果
co_yield表达式
co_yield
用于从协程产生值并暂停执行,特别适用于实现生成器模式:
cpp
#include <iostream>
#include <coroutine>
// 简化的生成器实现
template<typename T>
class Generator {
public:
struct promise_type {
T value;
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T val) {
value = val;
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
// 生成器方法
bool next() {
if (handle && !handle.done()) {
handle.resume();
return !handle.done();
}
return false;
}
T value() const { return handle.promise().value; }
// 资源管理
~Generator() { if (handle) handle.destroy(); }
Generator(Generator&& other) noexcept : handle(other.handle) { other.handle = {}; }
Generator& operator=(Generator&&) = delete;
Generator(const Generator&) = delete;
private:
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
std::coroutine_handle<promise_type> handle;
};
// 使用co_yield的生成器协程
Generator<int> fibonacci(int n) {
if (n > 0) co_yield 0;
if (n > 1) co_yield 1;
int a = 0, b = 1;
for (int i = 2; i < n; ++i) {
int next = a + b;
co_yield next;
a = b;
b = next;
}
}
co_yield
表达式在底层被转换为co_await
表达式:
co_yield x
基本等同于co_await promise.yield_value(x)
- 允许协程产生一个值并暂停,等待下一次恢复再继续执行
co_return语句
co_return
用于结束协程执行并指定返回值:
cpp
#include <iostream>
#include <coroutine>
#include <utility>
// 简单的返回值协程
template<typename T>
class Task {
public:
struct promise_type {
T result;
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T value) {
result = std::move(value);
}
void unhandled_exception() { std::terminate(); }
};
T result() const { return handle.promise().result; }
bool is_ready() const { return handle && handle.done(); }
// 资源管理
~Task() { if (handle) handle.destroy(); }
// ... 其他移动构造等
private:
explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
std::coroutine_handle<promise_type> handle;
};
// 使用co_return的协程
Task<std::string> compute_greeting(std::string name) {
std::string greeting = "Hello, " + name + "!";
co_return greeting; // 结束协程并返回结果
}
co_return
的行为取决于Promise类型的实现:
co_return;
(无值):调用promise.return_void()
co_return x;
(有值):调用promise.return_value(x)
- 之后协程执行进入最终阶段,调用
co_await promise.final_suspend()
Promise对象
Promise对象是C++20协程的核心组件,用于控制协程的行为。每个协程都关联一个Promise对象,由协程框架在协程创建时构造:
cpp
template<typename ReturnType>
struct MyPromise {
// 必需的成员
ReturnType get_return_object(); // 创建并返回协程的返回对象
std::suspend_always initial_suspend(); // 控制协程开始时是否立即执行
std::suspend_always final_suspend() noexcept; // 控制协程结束时的行为
// 与返回值相关的成员(二选一)
void return_void(); // 处理无返回值的co_return语句
void return_value(ReturnType value); // 处理有返回值的co_return语句
// 异常处理成员
void unhandled_exception(); // 处理协程中未捕获的异常
// 可选的其他成员
std::suspend_always yield_value(ValueType value); // 处理co_yield表达式
AwaitableType await_transform(OtherAwaitable a); // 自定义co_await行为
};
Promise对象的主要职责:
- 创建协程的返回对象
- 控制协程的初始化和最终化行为
- 处理协程的返回值
- 处理协程中的异常
- 处理
co_yield
表达式 - 自定义
co_await
表达式的行为
协程句柄
协程句柄(std::coroutine_handle<>
)是用于操作协程的非拥有者句柄,提供了与协程交互的接口:
cpp
struct MyPromise {
struct Task {
std::coroutine_handle<MyPromise> handle;
~Task() {
if (handle) handle.destroy(); // 必须手动销毁协程
}
};
Task get_return_object() {
return Task{std::coroutine_handle<MyPromise>::from_promise(*this)};
}
// ... 其他必需方法
};
// 协程句柄操作示例
auto task = some_coroutine();
std::coroutine_handle<MyPromise> handle = task.handle;
if (!handle.done()) {
handle.resume(); // 恢复协程执行
}
// 检查协程是否完成
bool finished = handle.done();
协程句柄的主要操作:
resume()
:恢复协程执行done()
:检查协程是否执行完毕destroy()
:销毁协程帧,释放资源promise()
:访问关联的Promise对象address()
:获取协程帧的地址
重要注意事项:
- 协程句柄不会自动销毁协程,必须手动调用
destroy()
- 对已完成或已销毁的协程调用
resume()
会导致未定义行为
协程的工作原理
协程状态
协程在执行过程中可能处于以下几种状态:
- 挂起状态(Suspended):协程暂停执行,等待被恢复
- 活动状态(Active):协程正在执行中
- 完成状态(Done):协程已执行完毕
状态转换:
- 创建后→初始挂起(根据
initial_suspend
决定) - 挂起→活动:通过
resume()
恢复执行 - 活动→挂起:通过
co_await
或co_yield
暂停 - 活动→完成:通过
co_return
或执行到函数末尾 - 完成→最终挂起(根据
final_suspend
决定)
协程帧
协程帧是存储协程状态的内存区域,包含以下内容:
- Promise对象:控制协程行为的对象
- 参数副本:协程参数的拷贝或引用
- 局部变量:协程中声明的局部变量
- 暂停点信息:当前执行位置的信息
- 恢复后要执行的代码地址:恢复执行时的下一条指令地址
协程帧的生命周期:
- 协程首次调用时在堆上分配
- 协程执行期间持续存在,即使协程暂停
- 调用
coroutine_handle::destroy()
时释放
挂起与恢复机制
协程挂起过程:
- 当执行到
co_await expr
时,首先计算表达式expr
得到等待者对象 - 调用等待者的
await_ready()
方法检查是否需要挂起 - 如果返回
false
,保存当前执行状态 - 调用等待者的
await_suspend(handle)
方法,可能安排后续恢复操作 - 让出控制权,返回到协程的调用者
协程恢复过程:
- 通过协程句柄的
resume()
方法恢复执行 - 恢复协程的执行状态(包括局部变量等)
- 调用等待者的
await_resume()
方法获取结果值 - 继续执行剩余的协程代码,直到下一个挂起点或结束
协程的生命周期
一个典型的协程生命周期包括以下阶段:
-
创建阶段:
- 分配协程帧
- 构造Promise对象
- 调用
promise.get_return_object()
获取返回对象 - 调用
co_await promise.initial_suspend()
决定是否立即挂起
-
执行阶段:
- 执行协程函数体
- 在
co_await
和co_yield
点暂停和恢复 - 处理各种操作和计算
-
完成阶段:
- 执行
co_return
语句或到达函数末尾 - 调用
promise.return_void()
或promise.return_value(x)
- 调用
co_await promise.final_suspend()
决定最终挂起行为
- 执行
-
销毁阶段:
- 调用
coroutine_handle::destroy()
销毁协程帧 - 释放所有资源
- 调用
构建协程类型
基础协程返回类型
创建一个自定义的协程返回类型通常包括以下步骤:
- 定义返回类型(比如
Task<T>
) - 定义该类型的
promise_type
内部类 - 实现必要的Promise方法
- 提供协程句柄管理机制
下面是一个简化但完整的异步任务类型实现:
cpp
template<typename T = void>
class Task {
public:
// Promise类型
struct promise_type {
std::variant<std::monostate, T, std::exception_ptr> result;
std::coroutine_handle<> continuation = nullptr;
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
auto final_suspend() noexcept {
struct FinalAwaiter {
bool await_ready() noexcept { return false; }
std::coroutine_handle<> await_suspend(
std::coroutine_handle<promise_type> h) noexcept {
// 如果有等待的协程,恢复它
if (auto cont = h.promise().continuation)
return cont;
return std::noop_coroutine();
}
void await_resume() noexcept {}
};
return FinalAwaiter{};
}
template<typename U>
void return_value(U&& value) {
result.template emplace<1>(std::forward<U>(value));
}
void unhandled_exception() {
result.template emplace<2>(std::current_exception());
}
};
// 等待器
auto operator co_await() const noexcept {
struct TaskAwaiter {
std::coroutine_handle<promise_type> handle;
bool await_ready() const noexcept { return handle.done(); }
std::coroutine_handle<> await_suspend(std::coroutine_handle<> h) noexcept {
handle.promise().continuation = h;
return handle;
}
T await_resume() {
auto& result = handle.promise().result;
if (result.index() == 2)
std::rethrow_exception(std::get<2>(result));
if constexpr (!std::is_void_v<T>)
return std::get<1>(std::move(result));
}
};
return TaskAwaiter{handle};
}
// 手动控制方法
void start() {
if (handle && !handle.done())
handle.resume();
}
bool is_done() const { return !handle || handle.done(); }
// 资源管理
~Task() { if (handle) handle.destroy(); }
Task(Task&& other) noexcept : handle(other.handle) { other.handle = nullptr; }
Task& operator=(Task&&) noexcept; // 实现省略
Task(const Task&) = delete;
Task& operator=(const Task&) = delete;
private:
explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
std::coroutine_handle<promise_type> handle;
};
// 示例协程函数
Task<int> compute() {
co_return 42;
}
Task<> use_value(int value) {
std::cout << "值: " << value << std::endl;
co_return;
}
Task<> example() {
int result = co_await compute();
co_await use_value(result);
}
实现生成器
生成器是协程的一个常见应用,用于惰性生成一系列值。以下是一个生成器实现的关键部分:
cpp
template<typename T>
class Generator {
public:
struct promise_type {
std::optional<T> current_value;
std::exception_ptr exception;
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = std::move(value);
return {};
}
void return_void() {}
void unhandled_exception() { exception = std::current_exception(); }
};
// 迭代方法
bool next() {
if (handle && !handle.done()) {
handle.resume();
if (handle.done()) return false;
return true;
}
return false;
}
T value() const { return *handle.promise().current_value; }
// 迭代器支持,使生成器可用于范围for循环
class iterator { /* 实现省略 */ };
iterator begin();
iterator end();
// 资源管理
~Generator() { if (handle) handle.destroy(); }
// ... 移动构造等
private:
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
std::coroutine_handle<promise_type> handle;
};
// 示例:斐波那契数列生成器
Generator<int> fibonacci(int n) {
if (n > 0) co_yield 0;
if (n > 1) co_yield 1;
int a = 0, b = 1;
for (int i = 2; i < n; ++i) {
int next = a + b;
co_yield next;
a = b;
b = next;
}
}
// 使用方式
void use_generator() {
for (int value : fibonacci(10)) {
std::cout << value << " "; // 输出: 0 1 1 2 3 5 8 13 21 34
}
}
自定义等待者
等待者(Awaiter)是实现co_await
运算符行为的对象,必须提供三个特定方法:
cpp
// 简单的延迟等待器
class Delay {
public:
explicit Delay(std::chrono::milliseconds duration) : duration_(duration) {}
bool await_ready() const noexcept {
return duration_.count() <= 0;
}
void await_suspend(std::coroutine_handle<> handle) const {
std::thread([handle, this]() {
std::this_thread::sleep_for(duration_);
handle.resume();
}).detach();
}
void await_resume() const noexcept {}
private:
std::chrono::milliseconds duration_;
};
// 使用延迟等待器
Task<> delay_example() {
std::cout << "开始" << std::endl;
co_await Delay{std::chrono::seconds(1)};
std::cout << "1秒后" << std::endl;
co_await Delay{std::chrono::milliseconds(500)};
std::cout << "再过0.5秒后" << std::endl;
}
自定义等待者需要实现的三个方法:
await_ready()
: 决定是否需要暂停协程await_suspend(handle)
: 安排何时恢复协程await_resume()
: 提供co_await
表达式的结果值
实际应用场景
异步I/O操作
协程特别适合处理异步I/O操作,简化传统的回调模式:
cpp
// 模拟异步文件操作
class AsyncFile {
public:
AsyncFile(const std::string& filename) : filename_(filename) {
std::cout << "打开文件: " << filename << std::endl;
}
~AsyncFile() {
std::cout << "关闭文件: " << filename_ << std::endl;
}
// 异步读取操作的等待者
class ReadOperation {
public:
ReadOperation(const std::string& filename, int size)
: filename_(filename), size_(size) {}
bool await_ready() const { return false; }
void await_suspend(std::coroutine_handle<> handle) {
// 模拟异步I/O操作
std::thread([this, handle]() {
std::cout << "异步读取文件: " << filename_ << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
// 模拟读取
data_ = std::string(size_, 'D');
handle.resume();
}).detach();
}
std::string await_resume() { return data_; }
private:
std::string filename_;
int size_;
std::string data_;
};
ReadOperation read(int size) {
return ReadOperation(filename_, size);
}
// 写入操作类似...
private:
std::string filename_;
};
// 异步文件处理协程
Task<> process_file(const std::string& input_file, const std::string& output_file) {
try {
AsyncFile input(input_file);
AsyncFile output(output_file);
// 异步读取
std::string data = co_await input.read(1024);
std::cout << "读取完成,数据大小: " << data.size() << " 字节" << std::endl;
// 处理数据
std::string processed_data = "处理后: " + data;
// 异步写入
co_await output.write(processed_data);
std::cout << "写入完成" << std::endl;
} catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << std::endl;
}
}
协程在异步I/O中的优势:
- 使异步代码看起来像同步代码
- 自然的错误处理机制
- 资源的自动管理
- 避免回调嵌套
并发任务控制
协程可以用于简化并发任务的控制和协调:
cpp
// 并发任务执行器
class TaskExecutor {
public:
// 提交任务并返回awaiter
template<typename Func>
auto submit(Func&& func) {
struct Awaiter {
Func func;
bool await_ready() const { return false; }
void await_suspend(std::coroutine_handle<> handle) {
std::thread([this, handle]() {
try {
result_ = func();
} catch (...) {
exception_ = std::current_exception();
}
handle.resume();
}).detach();
}
auto await_resume() {
if (exception_)
std::rethrow_exception(exception_);
return result_;
}
std::optional<std::invoke_result_t<Func>> result_;
std::exception_ptr exception_;
};
return Awaiter{std::forward<Func>(func)};
}
};
// 使用执行器
Task<> parallel_tasks() {
TaskExecutor executor;
auto task1 = executor.submit([]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 42;
});
auto task2 = executor.submit([]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
return std::string("Hello");
});
// 并发等待两个任务
int result1 = co_await task1;
std::string result2 = co_await task2;
std::cout << "结果: " << result1 << ", " << result2 << std::endl;
}
生成器与惰性计算
协程特别适合实现惰性计算的生成器:
cpp
Generator<int> range(int start, int end, int step = 1) {
for (int i = start; i < end; i += step) {
co_yield i;
}
}
Generator<std::string> file_lines(const std::string& filename) {
std::ifstream file(filename);
std::string line;
while (std::getline(file, line)) {
co_yield line;
}
}
// 无限序列,按需计算
Generator<int> primes() {
std::vector<int> found_primes;
co_yield 2; // 第一个质数
for (int n = 3; ; n += 2) { // 从3开始的奇数
bool is_prime = true;
for (int prime : found_primes) {
if (prime * prime > n) break; // 优化检查
if (n % prime == 0) {
is_prime = false;
break;
}
}
if (is_prime) {
found_primes.push_back(n);
co_yield n;
}
}
}
// 使用生成器
void use_generators() {
// 使用范围生成器
for (int i : range(0, 10, 2)) {
std::cout << i << " "; // 输出 0 2 4 6 8
}
// 读取文件的前10行
int count = 0;
for (const auto& line : file_lines("data.txt")) {
std::cout << line << std::endl;
if (++count >= 10) break;
}
// 获取前10个质数
count = 0;
for (int p : primes()) {
std::cout << p << " ";
if (++count >= 10) break;
}
}
状态机实现
协程可以优雅地实现状态机:
cpp
// 解析器状态机
Task<std::vector<std::string>> parse_csv(std::string_view data) {
std::vector<std::string> fields;
std::string current_field;
enum class State { Field, QuotedField, QuoteInQuotedField };
State state = State::Field;
for (char c : data) {
// 复杂状态处理逻辑
switch (state) {
case State::Field:
if (c == ',') {
fields.push_back(current_field);
current_field.clear();
co_await Delay{std::chrono::microseconds(1)}; // 允许状态检查
} else if (c == '"') {
state = State::QuotedField;
} else {
current_field += c;
}
break;
case State::QuotedField:
if (c == '"') {
state = State::QuoteInQuotedField;
} else {
current_field += c;
}
break;
case State::QuoteInQuotedField:
if (c == '"') {
current_field += '"';
state = State::QuotedField;
} else if (c == ',') {
fields.push_back(current_field);
current_field.clear();
state = State::Field;
co_await Delay{std::chrono::microseconds(1)}; // 允许状态检查
} else {
current_field += c;
state = State::Field;
}
break;
}
}
// 添加最后一个字段
fields.push_back(current_field);
co_return fields;
}
与现有库的集成
与std::future的集成
可以将协程与std::future
集成,简化异步操作:
cpp
// 使future可等待
template<typename T>
auto operator co_await(std::future<T>&& future) {
struct FutureAwaiter {
std::future<T> future;
bool await_ready() const {
return future.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
}
void await_suspend(std::coroutine_handle<> handle) {
std::thread([this, handle]() mutable {
future.wait();
handle.resume();
}).detach();
}
T await_resume() {
return future.get();
}
};
return FutureAwaiter{std::move(future)};
}
// 示例:使用std::async与协程
Task<> future_example() {
auto future = std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 42;
});
std::cout << "等待future..." << std::endl;
int result = co_await std::move(future);
std::cout << "结果: " << result << std::endl;
}
与异步网络库的结合
协程可以与异步网络库(如Asio)结合,简化网络编程:
cpp
// 简化的Asio协程适配器
template<typename R>
auto awaitable(std::function<void(std::function<void(R)>)> async_op) {
struct Awaiter {
std::function<void(std::function<void(R)>)> async_op;
R result;
bool await_ready() const { return false; }
void await_suspend(std::coroutine_handle<> handle) {
async_op([this, handle](R r) {
result = std::move(r);
handle.resume();
});
}
R await_resume() { return std::move(result); }
};
return Awaiter{std::move(async_op)};
}
// 假设的异步HTTP客户端
class HttpClient {
public:
using Callback = std::function<void(std::string)>;
// 异步HTTP GET请求
void async_get(const std::string& url, Callback callback) {
// 模拟异步HTTP请求
std::thread([url, callback = std::move(callback)]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::string response = "Response from " + url;
callback(response);
}).detach();
}
};
// 使用协程简化HTTP请求
Task<> fetch_data() {
HttpClient client;
std::cout << "发起请求..." << std::endl;
// 转换为可等待操作
std::string response = co_await awaitable<std::string>(
[&client](auto cb) {
client.async_get("https://example.com", std::move(cb));
}
);
std::cout << "收到响应: " << response << std::endl;
}
与事件循环的结合
协程可以集成到事件循环中,实现高效的非阻塞异步编程:
cpp
// 简化的事件循环
class EventLoop {
public:
using Task = std::function<void()>;
void post(Task task) {
std::lock_guard<std::mutex> lock(mutex_);
tasks_.push(std::move(task));
has_task_.notify_one();
}
void run() {
while (!stop_) {
Task task;
{
std::unique_lock<std::mutex> lock(mutex_);
has_task_.wait(lock, [this] { return !tasks_.empty() || stop_; });
if (stop_) break;
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
}
void stop() {
std::lock_guard<std::mutex> lock(mutex_);
stop_ = true;
has_task_.notify_one();
}
private:
std::queue<Task> tasks_;
std::mutex mutex_;
std::condition_variable has_task_;
bool stop_ = false;
};
// 事件循环协程适配器
class EventLoopAwaiter {
public:
EventLoopAwaiter(EventLoop& loop) : loop_(loop) {}
bool await_ready() const { return false; }
void await_suspend(std::coroutine_handle<> handle) {
loop_.post([handle]() { handle.resume(); });
}
void await_resume() {}
private:
EventLoop& loop_;
};
// 使用事件循环执行协程
Task<> event_loop_example(EventLoop& loop) {
std::cout << "开始" << std::endl;
co_await EventLoopAwaiter(loop);
std::cout << "第一次在事件循环中恢复" << std::endl;
co_await EventLoopAwaiter(loop);
std::cout << "第二次在事件循环中恢复" << std::endl;
co_await EventLoopAwaiter(loop);
std::cout << "第三次在事件循环中恢复" << std::endl;
}
性能考量
协程的开销
协程相比传统函数有一些额外开销:
- 内存分配:协程帧通常在堆上分配,虽然编译器可能优化某些情况
- 额外的函数调用 :如
await_ready
、await_suspend
等方法的调用 - 状态管理:保存和恢复协程状态的开销
但协程通常比其他异步编程方式(如线程)的开销要小得多。
零开销抽象的实现
C++20协程设计为"零开销抽象",遵循C++的"只为你使用的部分付费"原则:
- 未使用的协程特性不会产生开销
- 编译器可以对协程进行优化,例如:
- 内联协程函数以减少调用开销
- 在某些情况下避免堆分配
- 消除不必要的暂停/恢复操作
优化策略
优化协程性能的几种策略:
-
避免不必要的暂停点 :比如使用
await_ready()
返回true
避免不必要的暂停 -
减少内存分配:
cpp// 针对小协程帧使用固定大小的内存池 void* operator new(size_t size) { static thread_local std::array<byte, 1024> buffer; static thread_local size_t used = 0; if (size + used <= buffer.size()) { void* result = buffer.data() + used; used += size; return result; } return ::operator new(size); }
-
尽可能使用值类型而非引用计数智能指针,减少动态分配和引用计数操作
-
避免协程嵌套过深,每一层嵌套都会创建新的协程帧
最佳实践与陷阱
错误处理
协程中的错误处理策略:
-
使用标准的try-catch机制:
cppTask<> error_handling_example() { try { int result = co_await potentially_throwing_operation(); std::cout << "结果: " << result << std::endl; } catch (const std::exception& e) { std::cerr << "捕获到异常: " << e.what() << std::endl; } }
-
通过Promise对象的
unhandled_exception
方法传播异常:cppvoid unhandled_exception() { exception_ = std::current_exception(); }
-
使用错误码和
std::expected
代替异常,适合性能关键场景
取消操作
实现协程操作的取消机制:
cpp
class CancellationToken {
public:
bool is_cancelled() const {
return cancelled_.load(std::memory_order_acquire);
}
void cancel() {
cancelled_.store(true, std::memory_order_release);
}
private:
std::atomic<bool> cancelled_{false};
};
// 在协程中使用取消令牌
Task<> cancellable_operation(std::shared_ptr<CancellationToken> token) {
for (int i = 0; i < 10; ++i) {
if (token->is_cancelled()) {
std::cout << "操作被取消" << std::endl;
co_return;
}
std::cout << "步骤 " << i << std::endl;
co_await Delay{std::chrono::milliseconds(500)};
}
}
调试协程
调试协程的挑战与技巧:
- 打印生命周期事件,跟踪协程的创建、暂停、恢复和销毁
- 使用协程上下文信息,在每个暂停点记录状态
- 在Promise类型中添加调试辅助方法
例如,添加调试日志:
cpp
class DebugPromise {
public:
// ... 正常Promise方法
auto initial_suspend() {
std::cout << "协程开始[" << id_ << "]" << std::endl;
return std::suspend_always{};
}
auto final_suspend() noexcept {
std::cout << "协程结束[" << id_ << "]" << std::endl;
return std::suspend_always{};
}
private:
static inline std::atomic<int> next_id_ = 0;
int id_ = next_id_++;
};
避免的常见错误
使用协程时的常见陷阱:
-
协程句柄生命周期管理不当:
cpp// 错误:协程句柄被销毁,但协程尚未完成 void misuse_handle() { auto handle = some_coroutine().handle; handle.resume(); handle.destroy(); // 如果协程还在运行,会导致未定义行为 }
-
循环依赖导致内存泄漏:两个协程互相等待对方完成
-
忽略异常传播:异常如果未被处理,可能会导致程序终止
-
没有处理协程挂起时的资源释放问题:
cppTask<> resource_leak() { auto resource = acquire_resource(); // 获取资源 co_await some_operation(); // 如果挂起,resource可能未被正确释放 release_resource(resource); } // 更好的方式:使用RAII Task<> proper_cleanup() { auto guard = ResourceGuard(); // RAII包装器 co_await some_operation(); // 即使挂起,guard的析构函数也会在协程销毁时被调用 }
总结
C++20协程是现代C++中最具革命性的特性之一,它为异步编程提供了一种优雅而高效的解决方案。协程允许我们以看似同步的方式编写异步代码,大大提高了代码的可读性、可维护性和效率。
主要优势包括:
- 简化异步编程:使异步代码的结构与同步代码类似,避免回调地狱
- 高效的状态管理:无栈协程模型提供轻量级的执行控制转移
- 强大的抽象能力:可以封装各种异步操作为协程
- 与现有C++特性兼容:与异常处理、RAII、智能指针等特性无缝集成
- 灵活的定制性:提供低级机制,允许构建各种高级抽象
协程特别适合以下场景:
- 异步I/O操作
- 并发任务控制
- 生成器与惰性计算
- 状态机实现
- 事件驱动编程
虽然C++20协程的使用有一定的学习曲线,但一旦掌握,它将成为我们处理异步编程问题的强大工具。随着越来越多的库和框架提供协程支持,C++协程将在未来的软件开发中发挥越来越重要的作用。
在下一篇文章中,我们将探讨C++20的另一个重要特性:范围(Ranges)库,它如何革新C++的算法和容器交互方式。
这是我C++学习之旅系列的第五十篇技术文章。查看完整系列目录了解更多内容。