协程(Coroutines)是 C++20 标准中引入的一项重要特性,旨在简化异步编程、生成器和事件驱动模型的实现。相比传统的线程、回调或 Promise,协程提供了更直观、更可维护的代码结构。本文将从协程的基本概念、实现细节、使用场景以及性能考虑等方面进行系统梳理,帮助读者快速掌握并应用协程技术。
1. 协程的基本概念
在 C++ 之前,异步编程往往需要使用回调、线程或第三方库(如 Boost.Asio、std::future 等)。这些方案的缺点是代码层次分散、错误易错、难以组合。协程通过在函数内部挂起(yield)与恢复(resume)的方式,将程序的执行流程拆分为多个“段”,使得异步操作看似同步。
核心术语:
- 协程函数:使用
co_await、co_yield或co_return的函数,返回值类型为std::future、std::generator或std::task等。 - 挂起点:
co_await、co_yield、co_return所在位置,函数会在此处挂起。 - 状态机:编译器将协程函数转换为状态机对象,负责保存局部变量与执行点。
2. 典型实现方式
C++20 标准提供了三种协程返回类型,分别适用于不同的使用场景。
2.1 std::generator(生成器)
#include <coroutine>
#include <iostream>
#include <optional>
template<typename T>
struct generator {
struct promise_type {
std::optional <T> current;
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 = std::move(value); return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> coro;
generator(std::coroutine_handle <promise_type> h) : coro(h) {}
~generator() { if (coro) coro.destroy(); }
bool next() { return coro.resume(), !coro.done(); }
T value() { return std::move(*coro.promise().current); }
};
generator <int> fibonacci(int n) {
int a = 0, b = 1;
for (int i = 0; i < n; ++i) {
co_yield a;
std::tie(a, b) = std::make_pair(b, a + b);
}
}
使用方式:
for (auto val : fibonacci(10)) {
std::cout << val << ' ';
}
2.2 std::future(异步任务)
C++20 引入了 co_await 与 std::future 的集成。下面演示一个简单的异步计算:
#include <future>
#include <chrono>
std::future <int> async_add(int a, int b) {
co_return a + b; // 自动包装为 std::future <int>
}
int main() {
auto fut = async_add(5, 7);
std::cout << "Result: " << fut.get() << '\n';
}
若想与事件循环结合,可使用 co_await 对已完成的 std::future:
std::future <int> delayed(int ms, int value) {
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
co_return value;
}
2.3 自定义 Task(适用于事件循环)
如果你想在自己的事件循环中调度协程,最好自定义一个 Task 类型并实现 await_transform。以下为简化示例:
#include <coroutine>
#include <iostream>
#include <queue>
#include <chrono>
#include <thread>
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Task simple_task(int id) {
std::cout << "Task " << id << " start\n";
co_return;
}
在事件循环中:
std::queue<std::function<void()>> loop;
loop.push([]{ std::cout << "Hello from loop\n"; });
while (!loop.empty()) {
auto job = std::move(loop.front());
loop.pop();
job();
}
3. 使用场景
-
异步 I/O
与std::async或boost::asio结合,可让 I/O 代码像同步那样写。co_await在等待 I/O 时挂起,释放线程资源。 -
生成器
用于遍历大数据集、文件行、网络数据包等。生成器不需要一次性把所有数据加载到内存。 -
协程管道
多个协程串联形成数据流(类似 Go 的 channel),可实现流式处理、数据清洗等。 -
游戏循环
任务调度器 + 协程可以实现分帧、状态机、动画等功能。
4. 性能与注意事项
- 内存占用:协程对象会保存局部变量,若局部变量较大,建议使用
co_yield或co_await把数据传递给外部,而不是复制。 - 异常传播:协程内部抛出的异常会通过
promise_type::unhandled_exception处理,若未处理会调用std::terminate。可以在 promise 中自定义unhandled_exception。 - 上下文切换:协程切换相较于线程切换更轻量,但仍需避免频繁挂起/恢复。建议在协程内部执行的同步工作尽量快。
5. 示例:基于协程的 HTTP 客户端
下面给出一个使用 libcurl 的异步 HTTP 请求示例(伪代码,实际需要自行实现 CurlAwaitable):
#include <coroutine>
#include <curl/curl.h>
#include <iostream>
struct CurlAwaitable {
CURL* easy;
std::string buffer;
CURLcode result;
CurlAwaitable(CURL* e) : easy(e) {}
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
// 设置写回调
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, [](char* ptr, size_t size, size_t nmemb, void* userdata) {
auto& buf = *static_cast<std::string*>(userdata);
buf.append(ptr, size * nmemb);
return size * nmemb;
});
curl_easy_setopt(easy, CURLOPT_WRITEDATA, &buffer);
// 异步执行
curl_easy_perform(easy);
h.resume();
}
std::string await_resume() { return buffer; }
};
std::future<std::string> async_http_get(const std::string& url) {
CURL* easy = curl_easy_init();
curl_easy_setopt(easy, CURLOPT_URL, url.c_str());
co_return co_await CurlAwaitable(easy);
}
int main() {
auto fut = async_http_get("https://api.github.com");
std::cout << fut.get() << std::endl;
}
6. 结语
C++20 的协程为异步编程提供了更接近同步代码的写法,降低了错误率、提升了可读性。掌握协程的基本语法、返回类型与事件循环框架,将使你在处理 I/O、生成器、流式处理等任务时更加得心应手。建议从小型项目实验起,逐步引入协程到生产代码中,以便充分了解其优势与局限。祝你编码愉快!