在高并发网络服务器开发中,传统的基于回调或线程池的设计往往会导致回调地狱、线程上下文切换开销大以及错误处理复杂。C++20 引入的协程(co_await, co_yield, co_return)提供了一种更直观、更高效的异步编程模型。本文将从协程的基本概念入手,介绍如何在 C++20 环境下使用协程构建一个简易的高并发网络服务器,并讨论其优势与常见坑点。
1. 协程基础回顾
- Promise & Awaiter:协程函数返回的类型通常是
std::future、std::generator或自定义的 Awaitable 对象。协程内部使用co_await暂停执行,等待 Awaiter 完成后继续。 - 状态机化:编译器将协程拆解成状态机,减少了堆栈空间占用,并且只在需要时切换状态。
- 无上下文切换:协程切换是由程序逻辑驱动的轻量级状态机,不涉及线程上下文切换,避免了系统调度开销。
2. 设计思路
我们将实现一个基于 asio(Boost.Asio 或独立的 ASIO 库) 的 TCP 服务器。关键点是把 asio::async_read / async_write 的回调包装为 Awaitable,利用协程实现异步读取/写入的顺序执行。
#include <asio.hpp>
#include <coroutine>
#include <iostream>
namespace net = asio;
using asio::ip::tcp;
// Awaitable 封装
template<class CompletionToken>
struct awaitable_read {
tcp::socket& socket;
std::size_t size;
CompletionToken token;
std::vector <char> buffer;
awaitable_read(tcp::socket& s, std::size_t sz, CompletionToken tok)
: socket(s), size(sz), token(std::move(tok)), buffer(sz) {}
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
socket.async_read_some(
net::buffer(buffer),
[h](std::error_code ec, std::size_t n){
h.promise().ec = ec;
h.promise().n = n;
h.resume();
});
}
std::size_t await_resume() { return n; }
std::error_code ec;
std::size_t n;
};
template<class CompletionToken>
struct awaitable_write {
tcp::socket& socket;
std::vector <char> buffer;
CompletionToken token;
awaitable_write(tcp::socket& s, std::vector <char> buf, CompletionToken tok)
: socket(s), buffer(std::move(buf)), token(std::move(tok)) {}
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
asio::async_write(
socket,
net::buffer(buffer),
[h](std::error_code ec, std::size_t n){
h.promise().ec = ec;
h.promise().n = n;
h.resume();
});
}
std::size_t await_resume() { return n; }
std::error_code ec;
std::size_t n;
};
3. 协程服务处理器
struct session : std::coroutine_handle<> {
session(tcp::socket sock) : socket(std::move(sock)) {}
~session(){ if(socket.is_open()) socket.close(); }
// 协程入口
void operator()() {
try {
while(true) {
std::size_t n = co_await awaitable_read(socket, 1024, net::use_awaitable);
if(n == 0) break; // 连接关闭
std::string msg(socket.data(), n);
std::cout << "收到: " << msg << '\n';
// 简单回声
std::vector <char> out(msg.begin(), msg.end());
co_await awaitable_write(socket, std::move(out), net::use_awaitable);
}
} catch(const std::exception& e) {
std::cerr << "会话异常: " << e.what() << '\n';
}
}
tcp::socket socket;
};
4. 主循环与连接接受
int main() {
net::io_context io{1};
tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
while(true) {
tcp::socket socket(io);
acceptor.accept(socket);
std::make_shared <session>(std::move(socket))->operator()();
}
}
说明:
io_context的run()只在单线程环境中启动一次。若想利用多核可将io_context与std::thread或asio::thread_pool配合,或者直接使用io_context::run()并让协程在不同线程上调度。
5. 优势对比
| 方案 | 线程/回调 | 资源占用 | 错误处理 | 代码可读性 |
|---|---|---|---|---|
| 传统回调 | 单线程/多线程 | 高 | 复杂(try-catch、状态机) | 低 |
| 线程池 | 线程数固定 | 低 | 简单(同步代码) | 中 |
| 协程 | 单线程/多线程 | 极低 | 简洁(try-catch、await) | 高 |
- 省去堆栈分配:协程只在需要时分配栈帧,避免了大量
new/delete。 - 易于错误传播:使用
try-catch捕获所有异常,错误沿协程链向上传递。 - 自然顺序:异步流程与同步代码保持同一结构,减少逻辑混乱。
6. 常见坑点
- 协程悬挂:未在合适位置
co_await,导致协程一直挂起。 - 资源竞争:在多线程
io_context下,socket必须只被一个线程访问。 - 异常漏捕:在 Awaitable 内部未捕获
std::system_error,导致协程崩溃。 - 生命周期管理:如使用 `std::make_shared ` 时,要确保协程对象在回调完成前不被销毁。
7. 进一步优化
- 使用
asio::use_awaitable:直接将use_awaitable传给async_*接口,省去手写 Awaitable。 - 组合协程:将多步 IO 逻辑拆分成小协程,再在主协程中
co_await,实现模块化。 - 任务池:使用
asio::thread_pool与协程结合,减少线程数同时保持高并发。
8. 小结
C++20 协程为高并发网络服务器提供了极具表达力且高效的异步编程模型。通过将传统回调包装为 Awaitable,利用 co_await 可以写出顺序化、易维护的网络 I/O 代码。虽然协程仍然需要对事件循环和线程安全细节保持警惕,但相较于回调或线程池,协程在性能与可读性上都拥有显著优势。希望本文能为你在 C++20 环境下构建高并发网络服务器提供实用参考。