732 字
4 分钟
asio 的 strand 与取消:两件容易用错的事

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 代码时,先明确“哪些操作可以被丢弃、丢弃后哪些状态已经改变”,再决定在哪里挂超时。

asio 的 strand 与取消:两件容易用错的事
/posts/asio-strand-cancellation/
作者
fantasy-peak
发布于
2026-07-19
许可协议
CC BY-NC-SA 4.0