在咱日常搬砖中,异步编程那可是不可或缺的必备技能,特别是在搞网络请求、文件读写、界面响应这些需要高并发、低延迟的场景时。说到异步,大家可能对进程和线程都比较熟,但提到协程,有些人可能就有点懵了。

其实协程这货早就出现了,只不过语言层面支持得晚。直到C++20,官方才正式把它扶正,加入了co_awaitco_returnco_yield这几个关键字,算是给异步开发带来了“语法糖级别的福音”。

这篇文章呢,我就结合自己在工程实践中使用Boost.Asio协程的一些经验,给大家盘一盘这玩意儿怎么玩,顺带贴点真实代码,保证你看了就能上手。

一、进程 vs 线程 vs 协程 


先来整清楚几个概念的区别,毕竟选对工具才能干对活儿:

特征

进程

线程

协程

定义

独立执行单元,拥有自己的地址空间

同一进程内的执行单元,共享资源

用户态轻量级“线程”,由程序控制

资源消耗

高,每个进程都有独立内存等资源

中,线程间共享内存

低,协程共享线程资源

创建销毁成本

高,系统调度开销大

中等

极低,由框架或语言管理

控制复杂度

高,需系统参与调度

中等,需处理线程通信

低,通常一个线程内搞定

并发性能

中等,IPC通信开销大

高,适合并行计算

极高,切换开销小,适合高并发

简单总结一下应用场景:

  • 进程:适合资源隔离、大型服务组件之间通信;

  • 线程:多核并行、资源共享任务;

  • 协程:I/O密集型、高并发场景,比如Web服务器、网络应用、异步任务队列等。

咱搞应用开发的,进程基本吃灰,线程池倒是常备弹药库。至于协程?嘿!轻量到能随地召唤,网络请求撸串儿似的写,生命周期自己拿捏——这自由度,线程看了都流泪!

二、Boost.Asio:异步界的扛把子


如果你还在用C++17或者更低版本,那就别指望原生协程了。这时候就得靠 Boost.Asio 这个老牌库来续命了。它不仅支持异步I/O操作,还自带一套协程机制,兼容性拉满,哪怕不升级到C++20也能玩得飞起。

Boost.Asio 提供了非常强大的异步模型,通过回调、绑定器、协程等方式,让你轻松写出高性能、可维护的异步代码。

而且它的协程实现方式,简直像是“让异步变同步”的魔法。你可以用类似同步的方式去写异步逻辑,还能优雅地控制协程生命周期,完全不用操作系统插手,自由度极高。

Post vs CoSpawn —— 两种异步操作姿势你Pick谁?

在 Boost.Asio 中,有两个常见的异步操作方式:post 和 co_spawn。来,咱们掰扯掰扯它们之间的区别:

  • post:用来把任务提交到 io_context 上异步执行,相当于扔个任务进去,然后继续干别的事。

  • co_spawn:启动一个协程,让异步代码写起来更像同步逻辑,简直是懒人福音。

来看个例子你就懂了:

#include <boost/asio.hpp>#include <boost/asio/experimental/co_spawn.hpp>#include <boost/asio/experimental/detached.hpp>#include <iostream>#include <chrono>namespace asio = boost::asio;using namespace std::chrono_literals;// 一个简单的协程函数asio::awaitable<void> async_print(const std::string& message) {    co_await asio::this_coro::executor.sleep_for(1s);    std::cout << message << std::endl;}int main() {    asio::io_context io_context;    // 使用 post 提交任务    asio::post(io_context, []() {        std::cout << "Hello from post!\n";    });    // 使用 co_spawn 启动协程    asio::experimental::co_spawn(io_context,         async_print("Hello from coroutine!"),         asio::experimental::detached);    io_context.run();  // 开始跑起来    return 0;}

在这个例子里:

  • async_print是个协程函数,它会等待一秒再打印消息。

  • post把一个 lambda 函数扔进 io_context,直接打印。

  • co_spawn则是启动协程,第三个参数表示这个协程是“分离式”的,不需要等它完成。

是不是看着清爽多了?告别嵌套回调地狱,从此写异步就像写同步!

三、协程封装实战


下面这部分内容是我实际项目中对 Boost.Asio 协程相关功能的一些封装和优化,方便后续复用和调试。

命名空间简化 + 类型别名定义

namespace asio = boost::asio;using error_code = boost::system::error_code;template <typename T>using awaitable = boost::asio::awaitable<T>;constexpr cross::comm::StrictDetachedType detached;        // 默认使用严格分离模式constexpr cross::comm::TolerantDetachedType tol_detached;  // 宽容分离模式,出错会记录日志using boost::asio::use_awaitable;using boost::asio::experimental::awaitable_operators::operator&&;using boost::asio::experimental::awaitable_operators::operator||;using await_token_t = asio::as_tuple_t<asio::use_awaitable_t<>>;constexpr await_token_t await_token;

这段代码主要是为了简化命名空间引用、类型声明和操作符使用,减少重复代码,提升可读性。


AsyncWaitSignal —— 信号监听也能协程化

我们来看看如何用协程监听一个信号,并且支持超时机制。

AsyncWaitSignal:监听信号触发

template <typename CompletionToken, typename... SigArgs>auto AsyncWaitSignal(boost::asio::any_io_executor ex,                      boost::signals2::signal<void(SigArgs...)> *sig,                     CompletionToken &&token){  return boost::asio::async_initiate<CompletionToken, void(boost::system::error_code, SigArgs...)>([...](auto handler, auto ex, auto sig) mutable {      // 内部逻辑略...  }, token, std::move(ex), sig);}

这个函数的作用是异步监听某个信号触发,内部用了 async_initiate 来包装成异步操作,并确保回调在指定的执行器上下文中执行。

AsyncWaitSignalWithTimeout:加个定时器,超时就拜拜

template <typename CompletionToken, typename... SigArgs>auto AsyncWaitSignalWithTimeout(...){  return boost::asio::async_initiate<...>([...](auto handler, auto ex, auto sig, auto timeout) mutable {      // 创建定时器 + 并行组,哪个先完成就处理哪个      boost::asio::experimental::make_parallel_group(          timer->async_wait(boost::asio::deferred),          AsyncWaitSignal(ex, sig, boost::asio::deferred)      ).async_wait(...);  }, ...);}

这个函数就是在上面的基础上加了个定时器,如果超时还没收到信号,就返回超时错误。非常适合用于限制等待时间的场景。

AsyncConnectSignal:连接信号并转发到指定执行器

template <typename Handler, typename... SigArgs>boost::signals2::connection AsyncConnectSignal(...){  return sig->connect_extended([...](const auto &conn, SigArgs &&...args) mutable {      boost::asio::post(ex, [...]() {          if (conn.connected()) std::apply(handler, std::move(args));      });  });}

这个函数的作用是将用户定义的回调安全地连接到信号上,并确保回调在指定的执行器上下文中执行,避免线程竞争问题。


实战演练:写一个协程函数模拟支付流程

来个真实业务中的例子,看看怎么用协程写异步逻辑。

awaitable<void> mock_pay(std::string auth_code) {    auto [ec, out_trade_no] = co_await PayRequest::SimulateMchPay(auth_code, 1);    if (ec) {        LOG_E("sim mch pay fail, ec: {} out_trade_no: {}", ec, out_trade_no);    } else {        LOG_I("sim mch pay out_trade_no: {}", out_trade_no);    }    co_return;}

调用方式也很简单:

co_await mock_pay(auth_code);

解释一下:

  • co_await:挂起当前协程,等待异步操作完成;

  • awaitable:表示这个函数是个可等待对象;

  • co_return:结束协程,类似于 return,但专为协程设计。

是不是很像同步写法?但背后全是异步操作,爽不爽?

⏱️ 定时器搞等待?co_await一招封印!

// 在主线程造个2秒定时炸弹asio::steady_timer timeout(Threads::MainThread()->Executor(), std::chrono::seconds(2));  co_await timeout.async_wait(await_token); // 坐等爆炸! 

解析一下:

  • asio::steady_timer:这是 Boost.Asio 提供的一个高精度定时器类,用来做“延时触发”非常合适。

  • Threads::MainThread()->Executor():获取主线程的执行器(Executor),它决定了这个定时器在哪条线程上运行回调。

  • std::chrono::seconds(2):设定定时时间为2秒。

  • async_wait:这是一个异步等待方法,不会阻塞当前协程,而是挂起并等待时间到达。

  • await_token:用于控制异步等待行为的对象,通常就是我们之前定义的 use_awaitable 或者封装过的 token。

一句话总结:

这段代码的意思是:“我先歇会儿,等两秒后再继续干活”,而且不卡主线程,不掉帧,完美!

📡 等信号还带超时?协程版“双重保险”

// 等WiFi扫描结果:10秒不来就掀桌!auto [ec, result] = co_await comm::AsyncWaitSignalWithTimeout(    this_thread::Executor(),     SystemInterface::Instance()->NetworkScanWifiCompletedSig(),     std::chrono::seconds(10), // 死亡倒计时    await_token);

传统写法:回调地狱里疯狂嵌套 + 手动管理定时器取消 → 代码比毛线团还乱。

协程写法一行顶十行! 内置超时熔断机制,信号不来直接抛timed_out错误码,稳得一批!

背后的boost::signals2信号库(文档直达)才是真·幕后大佬,协程只是让它更好用了~

并发任务?|| 操作符直接开无双!

// 支付结果查询:轮询 OR 推送?谁快用谁!auto results = co_await (ShorkLinkQueryPayResult(auth_code)                          ||  // 江湖人称"或操作符",专治选择困难症                         AsyncWaitNetworkPushMessage(auth_code));

解析一下:

  • ShorkLinkQueryPayResult(auth_code):通过短连接轮询查询支付结果的协程函数。

  • AsyncWaitNetworkPushMessage(auth_code):等待后台推送消息的协程函数。

  • ||:这是 Boost.Asio 提供的并行组操作符之一,意思是“只要其中一个完成就返回结果”,就像 JavaScript 中的 Promise.any()

  • co_await:挂起当前协程,等待任意一个任务完成。

一句话总结:

这段代码的意思是:“我有两个任务,你俩谁先跑完我就用谁的结果,剩下的爱咋咋地”。

ShorkLinkQueryPayResult 示例 —— 轮询查单也能协程化

示例代码片段:

图片

解析一下:

  • 使用 while(true) 实现了一个轮询逻辑。

  • 每次调用 FetchPayResult 查询支付状态。

  • 如果成功就返回结果,否则等一会再查。

  • 所有过程都在协程中进行,完全非阻塞,结构清晰。

一句话总结:

这段代码的意思是:“我一边轮询一边等,直到拿到结果为止,协程让这一切变得很优雅”。

AsyncWaitNetworkPushMessage 示例 —— 推拉结合查支付结果

示例代码片段:

图片

解析一下:

  • 等待后台推送的支付结果信号。

  • 支持超时机制,防止无限等待。

  • 成功收到信号则返回结果,失败则返回超时或错误码。

一句话总结:

这段代码的意思是:“我在等推送,如果没等到就说明出事了”。

四、总结


协程 + Boost.Asio = 异步开发的神兵利器

功能

技术点

优势

定时等待

steady_timer + async_wait

非阻塞、精准控制时间

异步信号

AsyncWaitSignalWithTimeout

支持超时、多线程安全

并发模型

`&& /

网络请求

协程封装网络调用

同步风格写异步逻辑,逻辑清晰

信号处理

boost::signals2 + post

安全分发、跨线程处理

💬 最后唠一句:

用了协程之后你会发现,以前那些嵌套回调、状态机、线程锁啥的“祖传代码”,现在都能用几行 co_await 给干掉。异步编程从此不再“套娃”,而是一种享受

如果你也想写出这种又快又稳、逻辑清晰、维护方便的异步代码,赶紧上车,一起用 Boost.Asio 协程起飞吧!

点击下方关注【Linux教程】,获取 大厂学习路线、项目教程、简历模板、大厂面试题pdf文档、大厂面经、编程交流圈子等等。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐