协程(Coroutines)是 C++20 标准中引入的一项强大功能,旨在简化异步编程和并发代码的书写。与传统的回调或线程模型相比,协程提供了更接近同步代码的可读性,同时保持了高效的执行性能。本文将从协程的基本概念、关键语法、实现原理以及实际使用场景四个方面展开讨论,并给出一段完整的示例代码,帮助你快速上手。
1. 协程的基本概念
协程是一种特殊的函数,其执行可以在任意点被挂起(yield)并在稍后恢复。协程的执行上下文(包括局部变量、寄存器状态、栈帧等)会被保存,以便后续继续执行。协程本质上是一种轻量级的用户级线程,调度由程序员或库负责,而非操作系统内核。
在 C++20 中,协程通过以下三个核心概念实现:
co_await:表示等待一个可等待对象,协程会挂起,直到等待对象完成。co_yield:从协程返回一个值,并将协程挂起,等待下一次请求。co_return:终止协程并返回最终值。
2. 关键语法与实现细节
2.1 协程返回类型
协程函数必须返回一个支持协程的类型,通常是 std::future、std::generator 或自定义的 Task 类型。C++20 标准库中提供了 std::future 和 std::generator,但它们并不是协程的唯一实现方式。
std::future <int> async_add(int a, int b);
2.2 co_await 的使用
co_await 关键字需要与一个可等待对象配合使用。可等待对象需要实现 await_ready(), await_suspend(), await_resume() 三个成员函数。标准库中的 std::future 就满足此接口。
auto result = co_await some_future;
2.3 co_yield 的使用
co_yield 用于生成器(generator)场景,每次调用会返回一个值并挂起。
co_yield 42;
2.4 自定义协程类型
下面给出一个最小的 Task 协程包装器示例:
struct Task {
struct promise_type {
int 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(int v) { result_ = v; }
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> coro_;
int get() { return coro_.promise().result_; }
};
3. 实际使用案例
3.1 异步网络请求
假设我们使用一个异步 HTTP 库(例如 cpp-httplib 的异步 API):
#include <iostream>
#include <cpprest/http_client.h> // 用于示例,实际项目中可替换为任意异步库
Task fetch_url(const std::string& url) {
auto client = std::make_shared <http_client>(url);
auto resp = co_await client->request(methods::GET);
std::string body = co_await resp.extract_string();
co_return body.size();
}
3.2 并行计算
协程可以与 std::async 或自定义线程池配合,实现并行计算:
Task parallel_sum(const std::vector <int>& data) {
int sum1 = 0, sum2 = 0;
auto fut1 = std::async(std::launch::async, [&]() {
for (int i = 0; i < data.size() / 2; ++i) sum1 += data[i];
});
auto fut2 = std::async(std::launch::async, [&]() {
for (size_t i = data.size() / 2; i < data.size(); ++i) sum2 += data[i];
});
co_await fut1;
co_await fut2;
co_return sum1 + sum2;
}
3.3 生成器模式
使用 co_yield 可以轻松实现懒加载的序列:
std::generator <int> range(int start, int end) {
for (int i = start; i < end; ++i)
co_yield i;
}
使用时:
for (int n : range(0, 10)) {
std::cout << n << ' ';
}
4. 性能与注意事项
- 上下文切换成本:协程的挂起/恢复不涉及内核上下文切换,成本较低。但若协程内部仍然使用阻塞调用,效果有限。
- 内存占用:协程的状态机会生成额外的数据结构,编译器会将其布局在堆栈或堆中。对于长生命周期的协程,建议使用自定义的
promise_type来控制堆栈大小。 - 异常传播:
co_await、co_yield的异常需要在promise_type的unhandled_exception中处理,否则会调用std::terminate()。
5. 小结
C++20 的协程为现代 C++ 开发提供了一种统一且高效的异步编程模型。通过 co_await、co_yield 等关键字,开发者可以写出更接近同步逻辑的代码,同时享受并发执行带来的性能优势。掌握协程的基本语法、实现细节以及与现有库的配合方式,将大幅提升项目的可维护性和开发效率。祝你在 C++ 生态中玩得愉快,写出更优雅、更高效的代码!