Table of contents
Open Table of contents
TL;DR
Asio 最常见的 bug 源头是对象生命周期(异步操作引用了已析构的对象)和缓冲区生命周期(buffer 底层内存在操作完成前被释放)。strand 不是锁但胜似锁——用错了比不用更危险。timer 取消的语义和直觉不一致。这篇列出 10 个陷阱和 7 条生产 checklist。
陷阱
1. 对象生命周期悬空引用(最常见的 bug)
症状:段错误、随机崩溃、内存损坏,通常在高负载时偶发。
原因:异步操作的 handler 引用了一个已经被析构的对象。
// ❌ 错误:session 可能在 handler 执行前被析构
class session {
tcp::socket socket_;
std::array<char, 1024> buf_;
void start() {
socket_.async_read_some(asio::buffer(buf_),
[this](auto ec, auto n) {
// this 可能已经是野指针
process(n);
});
}
};
修复:enable_shared_from_this 模式,让 handler 持有 shared_ptr 保活对象。
// ✓ 正确:shared_ptr 保证对象活到 handler 执行完
class session : public std::enable_shared_from_this<session> {
void start() {
auto self = shared_from_this();
socket_.async_read_some(asio::buffer(buf_),
[self](auto ec, auto n) {
self->process(n); // self 持有 shared_ptr,对象一定存活
});
}
};
2. 缓冲区生命周期问题
症状:读到垃圾数据、段错误。
// ❌ 错误:msg 在函数返回时析构,buffer 悬空
void send(tcp::socket& socket) {
std::string msg = "hello";
async_write(socket, asio::buffer(msg), handler);
// msg 被析构,但 async_write 还没完成
}
asio::buffer() 创建的是非拥有式引用——它只存储指针和长度,不持有内存。
// ✓ 正确:用 shared_ptr 延长 msg 的生命周期
void send(tcp::socket& socket) {
auto msg = std::make_shared<std::string>("hello");
async_write(socket, asio::buffer(*msg),
[msg](auto ec, auto n) {
// msg 的 shared_ptr 在 handler 中保活
});
}
3. 忘记调用 io_context::run()
症状:程序不崩溃、不报错,就是什么都不做。
asio::io_context io;
tcp::socket socket(io);
socket.async_connect(endpoint, handler);
// 忘记 io.run()
// handler 永远不会被调用,程序静默退出
所有异步操作的 handler 只会在调用 run() 的线程上执行。没有 run(),handler 永远在队列里。
4. strand 误用导致竞争条件
症状:数据竞争、乱序执行,但只在多线程 run() 时出现。
asio::strand<asio::io_context::executor_type> strand(io.get_executor());
tcp::socket socket(io); // ← socket 没有绑定 strand
// ❌ 这两个 handler 没有通过 strand 分发
socket.async_read_some(buf, read_handler); // 可能在线程 A 执行
socket.async_write_some(buf, write_handler); // 可能在线程 B 同时执行
// ✓ 正确:通过 strand 绑定 handler
socket.async_read_some(buf,
asio::bind_executor(strand, read_handler));
socket.async_write_some(buf,
asio::bind_executor(strand, write_handler));
// 或者更好:从一开始就用 strand 构造 socket
tcp::socket socket(strand);
其他常见错误:
- 为同一个资源创建了多个 strand(每个 strand 独立,不互相序列化)
- 在 strand 外直接操作 strand 保护的数据
- 混用
dispatch和post导致意外重入
5. timer 取消的反直觉语义
场景 1:取消一个已到期但 handler 还在队列中的 timer。
asio::steady_timer timer(io, std::chrono::seconds(1));
timer.async_wait(handler);
// 1 秒后 timer 到期,handler 被入队到完成队列
// 此时调用 cancel()...
timer.cancel(); // 没有效果!handler 已经在队列中,会正常执行
cancel() 只影响正在等待的操作,不影响已经完成但尚未分发的 handler。
场景 2:修改 timer 到期时间隐式取消。
timer.expires_after(std::chrono::seconds(5));
timer.async_wait(handler_A);
// 修改到期时间 → handler_A 会被调用,ec 为 operation_aborted
timer.expires_after(std::chrono::seconds(10));
timer.async_wait(handler_B);
场景 3:timer 析构时 handler 仍被调用。
{
asio::steady_timer timer(io, std::chrono::seconds(5));
timer.async_wait([&timer](auto ec) {
// ❌ timer 已经析构了,这里访问 timer 是 UB
});
} // timer 析构,handler 会被调用(ec = operation_aborted),但 timer 已不存在
6. 异常从 handler 中传播
io.run(); // handler 抛异常会从这里传出来
如果一个 handler 抛异常:
- 异常从该线程的
run()传播出去 - 其他线程的
run()不受影响 io_context不会被 stop- 该线程可以重新调用
run()(不需要restart())
陷阱:以为一个 handler 的异常会停止整个事件循环。实际上只影响一个线程。
7. DNS resolver 的隐形问题
- 无缓存:每次
async_resolve都可能触发 DNS 查询。在高频场景下,DNS 可能成为瓶颈 - 阻塞:同步版
resolve()在 DNS 超时时会阻塞数秒 - 多地址:一个域名可能解析到多个 IP。如果只用第一个,在某个 IP 不可达时不会自动 fallback
- iterator 生命周期:
async_resolve返回的 results 应该在 handler 内立即消费
8. Handler 分配开销
每个异步操作可能为 handler 分配内存(::operator new)。在高频操作(如每秒数十万次 I/O)时,这是可测量的性能瓶颈。
// Asio 的保证:handler 的内存在调用前被释放
// 这意味着可以实现一个简单的回收分配器
class handler_memory {
std::aligned_storage_t<1024> storage_;
bool in_use_ = false;
public:
void* allocate(std::size_t size) {
if (!in_use_ && size <= sizeof(storage_)) {
in_use_ = true;
return &storage_;
}
return ::operator new(size);
}
void deallocate(void* p) {
if (p == &storage_) {
in_use_ = false;
} else {
::operator delete(p);
}
}
};
这之所以可行,是因为 Asio 保证「释放旧 handler 内存 → 调用 handler → handler 内发起新操作 → 分配新 handler 内存」这个顺序。旧内存在新分配时已经被释放了。
9. Work Guard 管理不当
auto work = asio::make_work_guard(io);
io.run(); // 永远不会返回!
// 忘记 work.reset(),无法优雅关闭
或者反过来:
// 没有 work guard
asio::steady_timer timer(io, std::chrono::seconds(60));
timer.async_wait(handler);
// 如果在 timer 到期前没有其他工作...
// run() 不会提前退出(timer 本身算 work),但如果 timer handler
// 内部又要发起新的 timer 等待,handler 执行和新 timer 注册之间
// 有微小的间隙,run() 可能在间隙中返回
10. 在 run() 还在运行时析构 io_context
asio::io_context io;
std::thread t([&] { io.run(); });
// ❌ 主线程退出时 io 被析构,但 t 还在调用 run()
// UB:析构非线程安全
// ✓ 正确:先停止,再 join,最后让 io 自然析构
io.stop();
t.join();
// io 在这之后析构是安全的
生产实践
1. 线程池大小
- I/O 密集型(大量连接、低 CPU 计算):1 个线程或少量线程调用
run() - CPU 密集型 handler:
std::thread::hardware_concurrency()个线程 - 不要过度订阅:线程数远超 CPU 核数会增加上下文切换开销
// 简单的线程池模式
asio::io_context io;
auto work = asio::make_work_guard(io);
unsigned n = std::thread::hardware_concurrency();
std::vector<std::thread> threads;
for (unsigned i = 0; i < n; ++i) {
threads.emplace_back([&io] { io.run(); });
}
// 关闭
work.reset();
for (auto& t : threads) t.join();
asio::thread_pool 是内置的替代方案(默认 hardware_concurrency() * 2 个线程)。
2. 连接管理模式
class server {
tcp::acceptor acceptor_;
asio::awaitable<void> accept_loop() {
for (;;) {
auto socket = co_await acceptor_.async_accept(asio::use_awaitable);
// 每个连接一个协程,自动管理生命周期
asio::co_spawn(acceptor_.get_executor(),
handle_connection(std::move(socket)),
asio::detached);
}
}
asio::awaitable<void> handle_connection(tcp::socket socket) {
auto strand = asio::make_strand(socket.get_executor());
// strand 保证这个连接的所有操作序列化
// 连接关闭时协程自然结束,资源自动释放
}
};
3. 错误处理策略
| 错误类型 | 处理方式 |
|---|---|
asio::error::operation_aborted | 正常取消,不记录错误 |
asio::error::eof | 对端正常关闭,清理连接 |
asio::error::connection_reset | 对端异常关闭,清理连接 |
asio::error::would_block | 内部处理,用户不应看到 |
| 其他系统错误 | 记录日志,视情况重试或关闭连接 |
// C++20 协程中推荐用 as_tuple 避免异常
auto [ec, n] = co_await socket.async_read_some(
asio::buffer(buf), asio::as_tuple(asio::use_awaitable));
if (ec == asio::error::eof) {
// 正常关闭
co_return;
}
if (ec) {
// 真正的错误
log_error(ec);
co_return;
}
4. 优雅关闭
asio::signal_set signals(io, SIGINT, SIGTERM);
signals.async_wait([&](auto ec, int sig) {
// 1. 停止接受新连接
acceptor.close();
// 2. 取消所有活跃连接的操作
for (auto& conn : active_connections) {
conn->cancel();
}
// 3. 释放 work guard
work.reset();
// 4. io_context::run() 会在所有 handler 执行完后自然返回
});
5. 超时控制
asio::awaitable<void> read_with_timeout(tcp::socket& socket,
asio::mutable_buffer buf,
std::chrono::seconds timeout) {
asio::steady_timer timer(socket.get_executor(), timeout);
// 同时等待读取和超时
using namespace asio::experimental::awaitable_operators;
auto result = co_await (
socket.async_read_some(buf, asio::use_awaitable)
|| timer.async_wait(asio::use_awaitable)
);
// variant index 0 = read 先完成, 1 = timer 先完成
}
6. 内存管理
- 缓冲区池:预分配固定大小的缓冲区,避免每次连接
new/delete - handler 内存回收:利用 Asio 的「释放在调用前」保证实现零分配 handler
- 避免 lambda 值捕获大对象:用
shared_ptr或引用
7. 调试工具
定义 BOOST_ASIO_ENABLE_HANDLER_TRACKING 启用 handler 追踪:
输出到 stderr 的格式:
@asio|1589487039.741377|0*1|socket@0x7fff...|async_read_some
@asio|1589487039.742128|>1| |
@asio|1589487039.742198|1*2|socket@0x7fff...|async_write
各字段含义:
- 0*1 = handler 0 创建了 handler 1
- >1 = handler 1 开始执行
- <1 = handler 1 执行结束
用 Asio 自带的 handlerviz.pl + GraphViz 可视化 handler 链:
# 需验证:工具是否仍在当前版本的 Asio 中提供
cat trace.log | perl handlerviz.pl | dot -Tpng > handlers.png
用 BOOST_ASIO_HANDLER_LOCATION 宏在追踪输出中嵌入源码位置信息。
Per-Operation Cancellation(Boost 1.77+)
细粒度取消,不需要关闭整个 socket:
asio::cancellation_signal cancel_signal;
socket.async_read_some(asio::buffer(buf),
asio::bind_cancellation_slot(cancel_signal.slot(),
[](auto ec, auto n) {
if (ec == asio::error::operation_aborted) {
// 被取消了
}
}));
// 在其他地方取消这个特定操作
cancel_signal.emit(asio::cancellation_type::total);
| 取消类型 | 含义 | 副作用 |
|---|---|---|
terminal | 取消后 I/O 对象只能 close/destroy | 无 |
partial | 可能有部分副作用 | 报告已传输字节数 |
total | 零可观察副作用 | 无 |