如何用Boost.Asio+C++20协程实现非阻塞耗时计算任务?
用Boost.Asio实现需求的最优低开销方案
一、耗时计算任务的主动让出与空闲恢复
要让耗时计算不阻塞io_context线程,同时在执行一段计算后主动让出,等io_context无其他任务时再继续,最省资源的方式就是用**asio::defer的可等待版本**。
asio::defer的作用是把后续计算任务放到执行器的延迟队列里,只有当io_context当前没有其他待处理任务时,才会调度执行这段计算。完全不需要定时器,无额外开销,还能保证io_context优先处理其他IO任务或高优先级任务,只有闲下来才继续计算。
在计算循环里,每完成一块计算后,用co_await asio::defer(executor)就能让协程挂起,把后续计算推迟到io_context空闲时执行。
二、一对多的协程等待事件实现
需要自定义一个CoroEvent类,支持多个协程异步等待结果,计算完成时一次性唤醒所有等待的协程,而且之后再进来的等待请求能直接拿到结果,不用挂起。
核心逻辑
- 用容器存储所有等待的回调函数;
- 用布尔值标记计算是否完成;
- 调用
async_wait时,如果计算已完成就直接触发回调,未完成则把回调加入等待队列; - 计算完成后调用
send,遍历所有回调并触发,同时标记事件已完成。
完整可运行代码
#include <boost/asio.hpp> #include <vector> #include <functional> #include <iostream> #include <mutex> namespace asio = boost::asio; // 支持一对多等待的协程事件类 class CoroEvent { public: explicit CoroEvent(asio::any_io_executor executor) : executor_(std::move(executor)) , signaled_(false) {} // 异步等待事件,兼容asio::use_awaitable template <typename CompletionToken> auto async_wait(CompletionToken&& token) { return asio::async_initiate<CompletionToken, void()>( [this](auto handler) { std::lock_guard<std::mutex> lock(mutex_); if (signaled_) { // 事件已触发,直接调度回调 asio::post(executor_, std::move(handler)); } else { // 事件未触发,将回调加入等待队列 handlers_.push_back(std::move(handler)); } }, token ); } // 触发事件,唤醒所有等待的协程 void send() { std::vector<std::function<void()>> handlers; { std::lock_guard<std::mutex> lock(mutex_); if (signaled_) return; signaled_ = true; handlers.swap(handlers_); // 转移队列所有权,避免持锁时执行回调 } // 逐个调度等待的回调 for (auto& handler : handlers) { asio::post(executor_, std::move(handler)); } } // 重置事件(如需重复使用) void reset() { std::lock_guard<std::mutex> lock(mutex_); signaled_ = false; handlers_.clear(); } private: asio::any_io_executor executor_; std::mutex mutex_; bool signaled_ = false; std::vector<std::function<void()>> handlers_; }; // 模拟耗时计算的结果类型 struct MyReturnType { int total = 0; // 模拟单次计算操作 void compute(size_t idx) { total += static_cast<int>(idx % 100); } }; MyReturnType calc_result; // 耗时计算协程 asio::awaitable<void> long_running_calc(asio::any_io_executor executor) { // 双层循环模拟大量计算任务 for (size_t i = 0; i < 1000; ++i) { for (size_t j = 0; j < 1000; ++j) { calc_result.compute(i * 1000 + j); } // 让出执行权,等待io_context空闲后继续 co_await asio::defer(executor); } // 计算完成,触发事件通知所有等待者 static CoroEvent finish_event(co_await asio::this_coro::executor); finish_event.send(); } // 获取计算结果的协程 asio::awaitable<MyReturnType> get_calc_result() { static CoroEvent finish_event(co_await asio::this_coro::executor); co_await finish_event.async_wait(asio::use_awaitable); co_return calc_result; } // 模拟其他IO任务,验证io_context不会被阻塞 asio::awaitable<void> sample_io_task(int task_id) { for (int i = 0; i < 5; ++i) { std::cout << "IO任务 " << task_id << " 正在运行\n"; // 模拟IO等待(实际场景替换为真实异步IO操作) co_await asio::steady_timer(co_await asio::this_coro::executor, std::chrono::milliseconds(100)).async_wait(asio::use_awaitable); } } int main() { asio::io_context io_ctx; // 启动计算任务 asio::co_spawn(io_ctx, long_running_calc(io_ctx.get_executor()), asio::detached); // 启动3个等待计算结果的协程 for (int i = 0; i < 3; ++i) { asio::co_spawn(io_ctx, [i]() -> asio::awaitable<void> { auto result = co_await get_calc_result(); std::cout << "等待者 " << i << " 获取到结果:" << result.total << "\n"; }, asio::detached); } // 启动2个IO任务,验证io_context的响应性 for (int i = 0; i < 2; ++i) { asio::co_spawn(io_ctx, sample_io_task(i), asio::detached); } // 运行io_context io_ctx.run(); return 0; }
三、方案优势
- 零额外开销:无需定时器,完全依托asio执行器调度,没有定时器的创建、销毁和轮询成本;
- io_context始终响应:计算任务仅在
io_context空闲时执行,确保其他IO任务能被及时处理; - 高效一对多等待:事件触发时一次性唤醒所有等待协程,后续等待请求直接返回结果,无需挂起。
其他协程库的类似机制
- libunifex:提供
manual_event类,支持多协程等待,触发时唤醒所有等待者; - CppCoro:通过
event类实现相同的多协程等待语义。
内容的提问来源于stack exchange,提问作者Fabian Keßler
相关产品推荐
相关产品推荐

