Boost.Asio 如何安全卸载协程任务并管理执行器生命周期?
结论
将阻塞或 CPU 密集型工作作为独立任务提交到工作线程,再通过 co_await co_spawn(..., use_awaitable) 等待结果。父协程仍由原来的 executor(例如 io_context 的 strand)驱动,工作任务则在指定的线程池中执行。关闭时,应等相关协程和工作任务完成后,再销毁它们使用的执行上下文。
io.poll_one() 只会尝试处理一个就绪的 handler,然后返回;这不代表整个协程或线程池中的工作都已结束。如果此时 main 返回并销毁 io_context,仍在运行的任务可能持有与它相关的执行器或异步操作状态。仅凭 io.run() 和 io.poll_one() 的差异,无法确定具体的崩溃原因,但不能把 poll_one() 当作所有任务完成的屏障。
将工作任务放到线程池
可以把同步计算或阻塞调用封装成一个在工作线程池上运行的 awaitable,再由父协程等待结果:
#include <boost/asio.hpp>
namespace asio = boost::asio;
int blocking_work(int input) {
// 在这里执行同步计算或阻塞 I/O。
return input * 2;
}
asio::awaitable<int> handle_request(int input, asio::thread_pool& workers) {
// 此协程由调用它的 executor 驱动。
int result = co_await asio::co_spawn(
workers,
[input]() -> asio::awaitable<int> {
co_return blocking_work(input);
},
asio::use_awaitable);
// use_awaitable 将结果交回等待它的协程。
co_return result;
}
实际使用时,应根据 Boost 版本和所用的 completion token 检查对应重载。如果工作不需要在协程内部执行异步操作,也可以通过 post、async_initiate 或其他异步封装在线程池中运行同步函数,再将结果作为完成通知交回原来的 executor。父协程需要等待工作任务完成,不能假设切换 executor 后,协程的所有状态都会自动迁移。
保持 strand 和取消访问有序
如果父协程由 strand 调度,就让它继续在该 strand 上处理 socket、timer 和共享状态。工作线程中的函数应通过参数接收所需数据,再通过任务完成机制返回结果。不要让工作线程和父协程在没有保护的情况下同时读写共享对象。
取消信号也要遵循明确的线程约束:在与相关操作状态相同的 strand 上发出,或使用适当的同步方式保护共享状态。不要假设 cancellation_signal、取消槽或协程内部状态可以由多个线程无同步地并发访问。Asio 通常无法强制中断正在执行的阻塞函数;如果需要提前停止,该函数必须支持协作式取消,并明确检查取消状态。
有序关闭执行上下文
stop() 可以阻止 io_context 继续分派 handler,但它不会等待工作线程池中的函数结束,也不保证所有协程都已完成。可以按以下顺序关闭:
- 停止接收新任务。
- 发出取消信号或关闭相关异步操作,并让完成 handler 有机会运行。
- 等待父协程结束,并确认它启动的工作任务也已完成。
- 停止并 join 工作线程池,再销毁
io_context和相关对象。
主事件循环可以等待 co_spawn 的完成回调返回,以确认顶层协程已经结束。线程池和 io_context 都应保持存活,直到可能访问它们的任务完成。示例中的 io.run() 会一直运行到没有待处理工作时才返回;改用 io.poll_one() 后立即退出,并不能提供相同的运行和完成保证。
如果设计要求立即调用 stop(),还应另行安排线程池任务的完成时间和执行上下文的存活期。stop() 本身不会排空或 join 不同 executor 上的任务。
备注:内容仅供参考。
