C++协程实战:异步编程代码实现(c++11 协程)
在咱日常搬砖中,异步编程那可是不可或缺的必备技能,特别是在搞网络请求、文件读写、界面响应这些需要高并发、低延迟的场景时。说到异步,大家可能对进程和线程都比较熟,但提到协程,有些人可能就有点懵了。
其实协程这货早就出现了,只不过语言层面支持得晚。直到C++20,官方才正式把它扶正,加入了co_await、co_return、co_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 协程起飞吧!