在 C++20 里,协程成为标准库的一部分,极大地简化了异步编程的写法。与传统回调、事件循环或多线程相比,协程可以用同步式代码写出异步逻辑,并保持代码的可读性。本文将介绍三方协程的基本结构、使用 std::generator 与 std::task 的技巧,以及如何将协程与线程池、事件循环结合,构建高性能可伸缩的应用。
1. 协程的核心概念
协程是可以挂起(co_await)和恢复(co_yield、co_return)的函数。它们生成一个可被外部控制的对象,内部维护执行状态。C++20 为协程提供了两种重要的返回类型:
- `std::generator `:返回一个可迭代的生成器,适合顺序产生一系列值。
- `std::task `:返回一个代表异步操作的任务,能够与 `co_await` 一起使用。
2. 实现一个自定义 std::task 类型
标准库中没有直接提供 std::task,但我们可以用 std::future、std::shared_future 或自定义 Promise/Handle 组合实现。下面给出一个简化的实现示例:
template<typename T>
struct Task {
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
struct promise_type {
T value_;
std::exception_ptr exc_;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
auto get_return_object() { return Task{handle_type::from_promise(*this)}; }
void unhandled_exception() { exc_ = std::current_exception(); }
template<typename U>
void return_value(U&& v) { value_ = std::forward <U>(v); }
};
Task(handle_type h) : h_(h) {}
~Task() { if (h_) h_.destroy(); }
T get() {
if (h_) {
h_.resume();
if (h_.done()) {
if (h_.promise().exc_) std::rethrow_exception(h_.promise().exc_);
return std::move(h_.promise().value_);
}
}
throw std::runtime_error("task not ready");
}
private:
handle_type h_;
};
此实现允许我们写:
Task <int> asyncAdd(int a, int b) {
co_return a + b;
}
3. 与线程池的协作
协程本身不占用线程,它们的挂起点会将控制权交还给调用者。要在多线程环境中真正并行执行,需要将协程调度到线程池。一个常见方案是:
- 创建
std::thread数量的工作线程,维护一个线程安全的任务队列。 co_await的操作会把协程句柄放入队列,线程池线程从队列取出并resume。- 当协程执行完毕后,线程池将其销毁或归还给回收池。
class ThreadPool {
std::vector<std::thread> workers_;
std::queue<Task<void>::handle_type> queue_;
std::mutex mtx_;
std::condition_variable cv_;
bool stop_ = false;
public:
ThreadPool(size_t n) {
for (size_t i=0; i<n; ++i)
workers_.emplace_back([this] { workerLoop(); });
}
void submit(Task <void>::handle_type h) {
std::lock_guard<std::mutex> lk(mtx_);
queue_.push(h);
cv_.notify_one();
}
void workerLoop() {
while (true) {
Task <void>::handle_type h;
{
std::unique_lock<std::mutex> lk(mtx_);
cv_.wait(lk, [this] { return stop_ || !queue_.empty(); });
if (stop_ && queue_.empty()) return;
h = queue_.front(); queue_.pop();
}
h.resume(); // 继续协程执行
}
}
~ThreadPool() {
stop_ = true;
cv_.notify_all();
for (auto& t : workers_) t.join();
}
};
4. 示例:并发网络请求
假设我们有一个异步 HTTP 客户端 fetchAsync(url) 返回 Task<std::string>,我们想并发获取 10 个 URL,并在全部完成后处理结果。代码示例:
ThreadPool pool(4); // 4 条工作线程
Task<std::vector<std::string>> fetchAll(const std::vector<std::string>& urls) {
std::vector<std::string> results;
for (const auto& u : urls) {
// 每个请求放到线程池执行
pool.submit(fetchAsync(u).get_return_object().handle_);
results.push_back(co_await fetchAsync(u)); // 简化示例
}
co_return results;
}
在实际代码中,fetchAsync 会在内部使用 co_await 等待网络 I/O,并把协程句柄返回给线程池,让工作线程继续执行。
5. 调试与性能提示
- 协程切换成本:每次
co_await都会产生一次上下文切换,尽量把耗时的操作放在 I/O 之外,例如使用std::async或线程池。 - 异常传播:协程内部抛出的异常会存入 promise,外部通过
Task::get()捕获。记得在协程函数中使用try/catch处理已知异常。 - 可伸缩性:协程本身非常轻量,关键是调度器。使用多核时,线程池的工作线程数可与 CPU 核数相匹配,或根据 I/O 密集度动态调整。
6. 结语
C++20 的协程为异步编程带来了革命性的简化。通过自定义 Task、配合线程池或事件循环,我们可以构建既易读又高效的并发程序。随着标准库的进一步完善,例如 std::async 的协程适配、std::ranges 与协程的深度结合,未来 C++ 的异步生态将更加成熟。希望本文能为你在项目中使用协程提供实用参考。