1. 项目概述:自定义awaiter对象的核心价值
在C++20协程的世界里,awaiter对象就像交响乐团的指挥家,它决定了协程何时该暂停演奏(挂起),何时该继续表演(恢复)。当我们为异步网络库手写awaiter时,实际上是在设计一套独特的"乐谱",让网络I/O操作能够与协程完美合拍。
传统异步回调的代码就像一场混乱的即兴演出,每个乐手(回调函数)都在等待不确定的信号,而协程配合awaiter则像精心编排的交响乐。下面这个简单的对比展示了二者的区别:
cpp复制// 传统回调地狱
socket.async_read(buffer, [&](error_code ec, size_t bytes) {
if (!ec) {
process_data(buffer, bytes);
socket.async_write(response, [](error_code ec, size_t) {
// 更多嵌套...
});
}
});
// 协程+awaiter版本
try {
size_t bytes = co_await socket.async_read(buffer);
process_data(buffer, bytes);
co_await socket.async_write(response);
} catch (const system_error& e) {
// 统一错误处理
}
2. 核心组件拆解:理解协程的四大支柱
2.1 协程函数与返回类型
任何包含co_await、co_yield或co_return的函数都会变成协程函数。但编译器需要知道如何管理这个协程的生命周期,这就是返回类型的职责:
cpp复制Task<void> my_coroutine() { // Task就是我们的返回类型
co_await something();
co_return;
}
Task类型必须包含一个嵌套的promise_type,它是协程真正的控制中心。promise_type就像协程的"大脑",决定了协程出生(构造)、死亡(析构)以及各种状态转换时的行为。
2.2 promise_type:协程的神经中枢
promise_type必须实现一组标准方法,下面是最关键的几个:
cpp复制struct promise_type {
Task get_return_object(); // 如何包装promise为返回对象
suspend_always initial_suspend(); // 创建后立即挂起?
suspend_always final_suspend() noexcept; // 结束后挂起?
void return_void(); // 处理co_return
void unhandled_exception(); // 异常处理
};
在实际网络编程中,我们通常会选择在initial_suspend挂起协程,以便控制其执行时机;而在final_suspend也挂起,以便安全清理资源。
2.3 coroutine_handle:协程的遥控器
std::coroutine_handle就像协程的遥控器,通过它我们可以:
cpp复制auto handle = std::coroutine_handle<promise_type>::from_promise(promise);
handle.resume(); // 继续执行协程
handle.destroy(); // 销毁协程帧
handle.done(); // 检查是否执行完毕
在网络库中,我们通常会把handle存储在事件循环中,当I/O事件就绪时调用resume()。
2.4 awaiter三件套:挂起控制的核心
一个完整的awaiter必须实现三个关键方法:
cpp复制struct MyAwaiter {
bool await_ready(); // 是否需要挂起
void await_suspend(coroutine_handle<>); // 挂起时做什么
auto await_resume(); // 恢复时返回什么
};
这三个方法构成了co_await表达式的完整生命周期控制,我们接下来会深入每个方法的实现细节。
3. 网络专用awaiter设计实战
3.1 SocketAwaiter类结构
为网络操作设计的awaiter需要包含以下核心字段:
cpp复制class SocketAwaiter {
public:
SocketAwaiter(IoContext& io_ctx, int fd, IoOperation op,
std::span<char> buf = {});
bool await_ready();
void await_suspend(std::coroutine_handle<> h);
int await_resume();
private:
IoContext& io_context_; // 事件循环引用
int fd_; // 套接字描述符
IoOperation op_; // 操作类型(读/写/连接)
std::span<char> buffer_; // 数据缓冲区
std::coroutine_handle<> coro_handle_; // 挂起的协程
int result_ = 0; // 操作结果(字节数)
int error_ = 0; // 错误码
};
3.2 await_ready的优化策略
await_ready()是避免不必要挂起的第一道防线。对于网络I/O,我们可以尝试一次非阻塞操作:
cpp复制bool SocketAwaiter::await_ready() {
if (op_ == IoOperation::Read) {
result_ = ::recv(fd_, buffer_.data(), buffer_.size(), MSG_DONTWAIT);
if (result_ >= 0) return true; // 立即有数据可读
if (errno != EWOULDBLOCK)
