C++协程实战:异步编程代码实现(c++11 协程)

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

其实协程这货早就出现了,只不过语言层面支持得晚。直到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 中,有两个常见的异步操作方式:postco_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 协程起飞吧!

相关文章

Qt多线程编程两种方式详解(qt开启线程的三种方式)

QT的多线程编程主要有两种方式:第一种是继承自QThread,然后重写run()函数;第二种是继承自QObject,然后把整个对象moveToThread;两种方法比较:第一种方法只有run()函数是...

Qt多线程创建(qt多线程直接处理数据)

【为什么要用多线程?】传统的图形用户界面应用程序都只有一个执行线程,并且一次只执行一个操作。如果用户从用户界面中调用一个比较耗时的操作,当该操作正在执行时,用户界面通常会冻结而不再响应。这个问题可以用...

正点原子I.MX6U嵌入式Qt开发指南:第十章《多线程》

今日头条/西瓜视频/抖音短视频 同名:正点原子原子哥今日头条/西瓜视频/抖音短视频账号:正点原子-原子哥感谢各位的关注和支持,你们的关注和支持是正点原子无限前进的动力。第十章《多线程》我们写的一个应用...

Qt 的4种多线程实现详解(qt实现多线程文件传输)

为何需要多线程?1、进行耗时操作时,可以处理用户的其他输入输出。比如,如果在UI线程里面进行耗时操作,界面会不响应用户操作。2、提升程序性能。现在的电脑一般都是多核CPU,多线程并行处理事务,可以大大...

Qt快速入门(工程的创建、UI界面布局、多线程、项目)

本文档将介绍QT工程的创建、UI界面布局,并以计数器为例了解QT中多线程的用法,最终完成一个基础的QT项目。1 创建QT工程文件在安装好QT之后,能够在其安装组件中找到Qt Creator,点击设置项...

Qt多线程1:QThread(Qt多线程通信)

1. Qt多线程概述Qt有两种多线程的方法,其中一种是继承QThread的run函数,另外一种是把一个继承于QObject的类转移到一个Thread里。 Qt4.8之前都是使用继承QThread的ru...