在C++20中,协程(coroutine)提供了一个极其简洁而强大的异步编程模型。与传统的线程或回调相比,协程能够让你以同步的写法描述异步逻辑,同时保持代码的可读性和可维护性。本文将以“网络请求异步下载”为例,演示如何使用C++20协程实现一个简易的异步下载器,并讨论其优势与常见陷阱。
1. 协程的基本概念
- co_await:挂起协程,等待一个 awaitable 对象完成。
- co_yield:产生一个值并挂起,类似生成器。
- co_return:返回值并结束协程。
- awaitable:任何可以被
co_await的类型,必须实现await_ready、await_suspend和await_resume三个成员函数。
协程本身并不创建新线程,协程的挂起和恢复是由调度器决定的,通常在事件循环(event loop)或线程池中完成。
2. 准备工作:简易异步网络请求库
下面我们使用一个假想的异步 HTTP 客户端 AsyncHttpClient,其 get 方法返回一个 std::future<std::string>。在真实项目中,你可以用 libcurl 的异步接口、Boost.Beast 或者自定义基于 epoll/kqueue 的 I/O 事件循环。
#include <future>
#include <string>
#include <chrono>
#include <thread>
#include <iostream>
class AsyncHttpClient {
public:
std::future<std::string> get(const std::string& url) {
return std::async(std::launch::async, [url]() {
// 模拟网络延迟
std::this_thread::sleep_for(std::chrono::milliseconds(200));
return std::string("Response from ") + url;
});
}
};
3. 协程包装器:future → awaitable
C++20 标准库没有直接提供将 std::future 转为 awaitable 的工具。我们手动实现一个轻量级的包装器:
#include <coroutine>
#include <future>
template<typename T>
struct FutureAwaitable {
std::future <T> fut;
bool await_ready() const noexcept { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([h, f = std::move(fut)]() mutable {
f.wait();
h.resume();
}).detach();
}
T await_resume() { return fut.get(); }
};
template<typename T>
FutureAwaitable <T> to_awaitable(std::future<T> f) {
return FutureAwaitable <T>{ std::move(f) };
}
说明
await_ready检查 future 是否已完成,若已完成则不挂起。await_suspend在一个独立线程中等待 future,然后恢复协程。实际项目可用事件驱动的完成机制替代。await_resume在协程恢复时返回 future 的结果。
4. 协程函数实现
下面是一个协程 download_file,接收 URL 并返回下载内容:
#include <string>
#include <coroutine>
#include <iostream>
struct DownloadTask {
struct promise_type {
std::string result;
DownloadTask get_return_object() { return {std::coroutine_handle <promise_type>::from_promise(*this)}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_value(std::string r) { result = std::move(r); }
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> coro;
std::string get() { return coro.promise().result; }
~DownloadTask() { if (coro) coro.destroy(); }
};
DownloadTask download_file(AsyncHttpClient& client, const std::string& url) {
std::string resp = co_await to_awaitable(client.get(url));
co_return resp;
}
关键点
DownloadTask的promise_type简单地保存结果。initial_suspend与final_suspend均返回std::suspend_never,表示协程在入口和出口都不挂起。实际场景中可根据需要改为std::suspend_always。co_await的 awaitable 是上面定义的FutureAwaitable。
5. 调度与事件循环
在本示例中,我们直接等待协程完成。真实应用往往需要一个事件循环,类似 Node.js 的 libuv。下面给出一个极简事件循环,处理协程挂起/恢复:
#include <queue>
#include <functional>
class EventLoop {
public:
void schedule(std::function<void()> task) { tasks.push(std::move(task)); }
void run() {
while (!tasks.empty()) {
auto t = std::move(tasks.front());
tasks.pop();
t();
}
}
private:
std::queue<std::function<void()>> tasks;
};
将 FutureAwaitable 的 await_suspend 修改为使用 EventLoop:
void await_suspend(std::coroutine_handle<> h) {
// 将恢复操作加入事件循环
event_loop.schedule([h](){ h.resume(); });
}
这样,所有挂起的协程都交由事件循环调度,避免了每次挂起都创建新线程。
6. 完整示例
#include <iostream>
#include <future>
#include <coroutine>
#include <string>
#include <thread>
#include <chrono>
#include <queue>
#include <functional>
// 事件循环
class EventLoop {
public:
void schedule(std::function<void()> task) { tasks.push(std::move(task)); }
void run() {
while (!tasks.empty()) {
auto t = std::move(tasks.front());
tasks.pop();
t();
}
}
private:
std::queue<std::function<void()>> tasks;
};
EventLoop g_loop;
// 异步 HTTP 客户端
class AsyncHttpClient {
public:
std::future<std::string> get(const std::string& url) {
return std::async(std::launch::async, [url]() {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
return std::string("Response from ") + url;
});
}
};
// Future → Awaitable
template<typename T>
struct FutureAwaitable {
std::future <T> fut;
bool await_ready() const noexcept { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
void await_suspend(std::coroutine_handle<> h) {
// 用事件循环恢复
g_loop.schedule([h](){ h.resume(); });
}
T await_resume() { return fut.get(); }
};
template<typename T>
FutureAwaitable <T> to_awaitable(std::future<T> f) { return FutureAwaitable<T>{std::move(f)}; }
// 协程任务
struct DownloadTask {
struct promise_type {
std::string result;
DownloadTask get_return_object() { return {std::coroutine_handle <promise_type>::from_promise(*this)}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_value(std::string r) { result = std::move(r); }
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> coro;
std::string get() { return coro.promise().result; }
~DownloadTask() { if (coro) coro.destroy(); }
};
DownloadTask download_file(AsyncHttpClient& client, const std::string& url) {
std::string resp = co_await to_awaitable(client.get(url));
co_return resp;
}
int main() {
AsyncHttpClient client;
auto task = download_file(client, "https://example.com");
g_loop.schedule([t=std::move(task)]() mutable {
std::cout << "Download finished: " << t.get() << std::endl;
});
g_loop.run(); // 进入事件循环
return 0;
}
运行结果
Download finished: Response from https://example.com
7. 优点与注意事项
| 优点 | 说明 |
|---|---|
| 代码简洁 | co_await 让异步代码呈现同步写法 |
| 可组合性 | 协程可以像普通函数一样被组合、传参 |
| 资源友好 | 只要有事件循环,协程本身不消耗线程资源 |
| 注意 | 说明 |
|---|---|
| awaitable 设计 | 必须正确实现 await_ready / await_suspend / await_resume,否则可能导致死锁 |
| 事件循环 | 协程挂起时应加入事件循环,避免使用 std::thread 阻塞 |
| 错误传播 | unhandled_exception 必须妥善处理,否则会直接终止程序 |
8. 小结
本文通过一个简易的网络下载示例,演示了 C++20 协程的基本使用方法。通过 FutureAwaitable 将 std::future 转为 awaitable,结合事件循环,你可以轻松实现高性能的异步 I/O。随着 C++23 引入的 std::ranges::subrange 与 std::experimental::generator,协程的生态将进一步完善,成为构建高并发应用的核心技术之一。