asio 里两个最常被误解的机制是 strand 和取消。前者经常被当成互斥锁的替代品,后者经常被当成“随时能停”的开关。两者都只提供比想象中更窄的保证。
strand 保证的是执行顺序
asio::make_strand(ex) 返回一个执行器,绑定到它的 handler 不会同时运行。它不等于“共享对象线程安全”——如果同一个对象也被 strand 之外的代码访问,竞争依然存在。
auto strand = asio::make_strand(io);asio::co_spawn(strand, session_loop(conn), asio::detached);在协程里,co_spawn 的 executor 会传播给协程内部发起的异步操作。但如果协程里又手动 post 到别的 executor,或者把对象交给另一个线程,strand 就保护不到那段代码。
一个常见错误是:用一个 strand 保护连接对象,却在另一个回调里直接读写它,理由是“它们都在 io_context 上”。io_context 只保证回调不会在同一个线程里重入,不保证不会换线程执行。
取消是协作式的
asio 的取消通过 cancellation_signal / cancellation_slot 传播。发起操作的一方把 slot 交给异步操作,另一方在需要时 emit:
asio::cancellation_signal signal;
asio::co_spawn(io, [&]() -> asio::awaitable<void> { auto slot = signal.slot(); auto state = co_await asio::this_coro::cancellation_state; // ...}, asio::detached);协程要真正响应取消,必须在挂起点检查 state.cancelled(),或者使用会订阅 slot 的组合操作。一个普通的 co_await socket.async_read_some(...) 只有在底层操作支持取消时才会被中断;你自己写的循环不会因为信号被 emit 就自动停下。
组合操作被取消后,状态是“半成品”
awaitable_operators 的 || 在第一个操作完成时取消另一个:
using namespace asio::experimental::awaitable_operators;
auto result = co_await (socket.async_read_some(buf, asio::use_awaitable) || timer.async_wait(asio::use_awaitable));如果超时先到,读操作被取消,但已经读进 buf 的字节不会回滚,buf 的偏移也没有更新。调用方如果重试,必须从“读了多少”这个显式状态继续,而不是假设什么都没发生。
同理,写操作被取消时,对端可能已经收到一部分数据。HTTP/2 或 chunked 编码里,这意味着连接通常不能复用。
一个可用的模式
把“可取消的等待”和“不可中断的提交”分开:
- 等待阶段用组合操作加超时,取消是安全的;
- 一旦开始写,就把整个写入放进一个独立协程,让它自己跑完;取消只取消等待,不取消已经提交的 I/O。
co_await (ready_ || deadline);// 到这里要么 ready,要么超时;超时就不要再碰 buf结论
strand 是执行器的串行化,不是对象的所有权边界;取消是订阅式的信号,不是抢占式的终止。写 asio 代码时,先明确“哪些操作可以被丢弃、丢弃后哪些状态已经改变”,再决定在哪里挂超时。