作为一名长期奋战在C++一线的开发者,当我第一次看到C++20协程规范时,立刻意识到这将彻底改变我们编写异步代码的方式。还记得2019年调试一个基于回调的异步网络服务时,那层层嵌套的回调让我不得不打印出完整的调用栈才能理清逻辑。而现在,同样的功能用协程实现后,代码量减少了40%,可读性提升了不止一个量级。
协程(Coroutine)本质上是一种可暂停和恢复的函数,它允许我们在不阻塞线程的情况下等待异步操作完成。与传统的线程相比,协程的上下文切换成本极低——一个典型的线程上下文切换需要约1-5微秒,而协程切换通常只需几十纳秒。这意味着我们可以在单个线程上并发运行数万个协程,这在IO密集型应用中优势尤为明显。
关键理解:协程不是线程的替代品,而是互补关系。CPU密集型任务仍需要多线程并行,而IO密集型任务则更适合用协程处理。
C++20协程的实现基于三个核心组件:协程帧(coroutine frame)、承诺对象(promise object)和协程句柄(coroutine handle)。当编译器遇到包含co_await、co_yield或co_return的函数时,会自动将其转换为状态机。
让我们看一个典型协程的内存布局示例:
code复制+-------------------+
| 协程帧 |
| |
| +---------------+ |
| | promise_type | |
| +---------------+ |
| | 局部变量 | |
| | 参数 | |
| | 挂起点信息 | |
| +---------------+ |
+-------------------+
协程帧的生命周期管理是自动的,当协程最终完成时(通过co_return或异常),帧会被自动销毁。这也是为什么我们通常不需要手动管理协程内存。
每个协程都必须有一个关联的promise_type,它控制着协程的关键行为。下面是一个典型promise_type的实现要点:
cpp复制struct Generator::promise_type {
// 协程首次挂起行为
auto initial_suspend() {
return std::suspend_always{}; // 通常立即挂起以延迟执行
}
// 协程最终挂起行为
auto final_suspend() noexcept {
return std::suspend_always{}; // 保持挂起以便清理
}
// 异常处理
void unhandled_exception() {
exception = std::current_exception();
}
// 协程返回值处理
void return_void() {}
// yield值处理
auto yield_value(int value) {
current_value = value;
return std::suspend_always{};
}
std::exception_ptr exception;
int current_value;
};
std::coroutine_handle是操作协程的核心工具,主要方法包括:
resume():恢复协程执行done():检查协程是否完成destroy():显式销毁协程promise():获取关联的promise对象典型用法示例:
cpp复制auto h = std::coroutine_handle<promise_type>::from_promise(promise);
if (!h.done()) {
h.resume(); // 恢复协程执行
}
让我们实现一个实用的异步文件读取器,展示协程在实际项目中的应用:
cpp复制#include <fstream>
#include <vector>
#include <coroutine>
#include <memory>
struct AsyncReadResult {
std::vector<char> data;
bool success;
};
struct AsyncFileReader {
struct promise_type {
std::unique_ptr<std::ifstream> file;
std::vector<char> buffer;
std::exception_ptr exception;
AsyncFileReader get_return_object() {
return AsyncFileReader{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
void return_void() {}
void unhandled_exception() { exception = std::current_exception(); }
auto yield_value(std::vector<char> data) {
buffer = std::move(data);
return std::suspend_always{};
}
};
std::coroutine_handle<promise_type> handle;
explicit AsyncFileReader(std::coroutine_handle<promise_type> h) : handle(h) {}
~AsyncFileReader() { if (handle) handle.destroy(); }
AsyncReadResult read(const std::string& filename, size_t chunk_size = 4096) {
handle.promise().file = std::make_unique<std::ifstream>(
filename, std::ios::binary);
if (!*handle.promise().file) {
return { {}, false };
}
std::vector<char> result;
while (!handle.done()) {
handle.resume();
if (handle.promise().exception) {
std::rethrow_exception(handle.promise().exception);
}
result.insert(result.end(),
handle.promise().buffer.begin(),
handle.promise().buffer.end());
}
return { std::move(result), true };
}
};
AsyncFileReader async_read_file(size_t chunk_size) {
std::vector<char> buffer(chunk_size);
while (true) {
auto& file = *co_await std::suspend_always{};
file.read(buffer.data(), buffer.size());
if (file.gcount() == 0) break;
buffer.resize(file.gcount());
co_yield buffer;
}
}
使用示例:
cpp复制int main() {
auto reader = async_read_file(1024);
auto result = reader.read("large_file.bin");
if (result.success) {
std::cout << "Read " << result.data.size() << " bytes\n";
} else {
std::cerr << "Failed to open file\n";
}
return 0;
}
cpp复制struct CoroutineMemoryPool {
static constexpr size_t POOL_SIZE = 1024;
std::array<std::byte, POOL_SIZE> pool;
bool used = false;
void* allocate(size_t size) {
if (!used && size <= POOL_SIZE) {
used = true;
return pool.data();
}
return ::operator new(size);
}
void deallocate(void* ptr) {
if (ptr == pool.data()) {
used = false;
} else {
::operator delete(ptr);
}
}
};
cpp复制void resume_coroutines(std::vector<std::coroutine_handle<>>& handles) {
for (auto& h : handles) {
if (!h.done()) {
h.resume();
}
}
handles.clear();
}
现代服务器通常采用单线程事件循环+多工作线程的混合模式。协程在这种架构中能发挥最大价值:
code复制主线程
├── 事件循环 (epoll/kqueue)
│ ├── 接收新连接 → 创建工作协程
│ └── IO事件 → 恢复对应协程
└── 工作线程池
├── CPU密集型任务
└── 协程调度
典型工作流程:
以Boost.Asio为例,我们可以轻松将其与协程结合:
cpp复制#include <boost/asio.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <coroutine>
using namespace boost::asio;
using namespace boost::asio::experimental::awaitable_operators;
awaitable<void> handle_connection(ip::tcp::socket socket) {
try {
streambuf buf;
for (;;) {
auto n = co_await async_read_until(
socket, buf, '\n', use_awaitable);
// 处理请求
std::string data{
buffers_begin(buf.data()),
buffers_begin(buf.data()) + n};
buf.consume(n);
// 模拟耗时操作
co_await timer::steady_timer{
socket.get_executor(),
std::chrono::milliseconds(100)}.async_wait(use_awaitable);
co_await async_write(
socket, buffer("OK: " + data), use_awaitable);
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
}
}
awaitable<void> listen(io_context& io, unsigned short port) {
auto executor = co_await this_coro::executor;
ip::tcp::acceptor acceptor(io, {ip::tcp::v4(), port});
for (;;) {
auto socket = co_await acceptor.async_accept(use_awaitable);
co_spawn(executor,
handle_connection(std::move(socket)),
detached);
}
}
int main() {
io_context io;
co_spawn(io, listen(io, 8080), detached);
io.run();
return 0;
}
协程生命周期管理:
cpp复制struct ScopedCoroutine {
std::coroutine_handle<> handle;
~ScopedCoroutine() {
if (handle && !handle.done()) {
handle.destroy();
}
}
};
异常处理:
cpp复制void unhandled_exception() {
exception = std::current_exception();
}
内存泄漏检测:
cpp复制void* operator new(size_t size) {
std::cout << "Allocating " << size << " bytes\n";
return ::operator new(size);
}
协程帧大小优化:
__builtin_coro_size预估帧大小批量IO操作:
cpp复制awaitable<std::vector<result>> batch_fetch(
std::vector<request> requests) {
std::vector<awaitable<result>> tasks;
for (auto& req : requests) {
tasks.push_back(async_fetch(req));
}
co_return co_await when_all(std::move(tasks));
}
协程与线程池配合:
cpp复制template <typename F>
auto schedule_on(thread_pool& pool, F f) {
struct Awaitable {
thread_pool& pool;
F f;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
pool.push([h, this] {
f();
h.resume();
});
}
void await_resume() {}
};
return Awaitable{pool, std::move(f)};
}
Java在JDK19引入了虚拟线程(Virtual Threads),与C++协程有相似目标但实现不同:
| 特性 | C++20协程 | Java虚拟线程 |
|---|---|---|
| 调度方式 | 手动控制 | JVM调度 |
| 内存开销 | 约几百字节 | 约2-4KB |
| 上下文切换成本 | ~50ns | ~100-200ns |
| 与现有代码集成 | 需要修改函数签名 | 基本透明 |
| 异常处理 | 通过promise_type | 标准try-catch |
| 调试支持 | 有限 | 完整堆栈跟踪 |
Go的goroutine是更高级别的抽象:
而C++协程:
C++20的Concept可以与协程结合,创建更安全的异步接口:
cpp复制template <typename T>
concept Awaitable = requires(T t, std::coroutine_handle<> h) {
{ t.await_ready() } -> std::convertible_to<bool>;
{ t.await_suspend(h) };
{ t.await_resume() };
};
template <Awaitable A>
auto operator co_await(A&& a) {
return std::forward<A>(a);
}
由于协程的堆栈不连续,传统调试方法可能失效。推荐技巧:
cpp复制struct CoroutineTracker {
static std::atomic<size_t> next_id;
size_t id;
CoroutineTracker() : id(next_id++) {
std::cout << "Coroutine " << id << " created\n";
}
~CoroutineTracker() {
std::cout << "Coroutine " << id << " destroyed\n";
}
};
cpp复制struct TracedHandle {
std::coroutine_handle<> handle;
std::vector<std::string> trace;
void resume() {
trace.push_back(get_current_stack());
handle.resume();
}
};
游戏引擎通常需要处理大量并发的逻辑更新,协程非常适合这种场景:
cpp复制awaitable<void> npc_behavior(NPC& npc) {
while (npc.is_alive()) {
// 移动到随机位置
co_await navigate_to(npc, random_position());
// 等待5-10秒
co_await wait_for_seconds(random(5, 10));
// 播放动画
co_await play_animation(npc, "idle");
}
}
void update_game() {
for (auto& npc : npcs) {
co_spawn(executor, npc_behavior(npc), detached);
}
}
这种模式使得游戏逻辑的编写更加直观,避免了状态机的复杂性。