当前位置:

首页 > 编程开发 > `co_await` 按下暂停键之后:从零看懂 C++20 协程

`co_await` 按下暂停键之后:从零看懂 C++20 协程

C++20协程通过编译器生成的状态机实现暂停与恢复,协程帧保存局部变量和执行位置。promise_type和awaiter定义了暂停与恢复协议,调度器协调线程执行就绪协程。核心难点在于协程帧生命周期管理,需确保只有一个出口负责恢复,并覆盖所有异常情况。

协程最吸引人的地方,在于它能用接近同步代码的写法来表达异步流程:

`co_await` 按下暂停键之后:从零看懂 C++20 协程

 复制代码auto data = co_await socket.read();
auto result = parse(data);
co_await socket.write(result);

代码从上到下,像普通函数一样自然;但在等待网络数据时,线程并没有停在那里干等着。

这看起来有点像魔法,其实背后无非三件事:编译器生成状态机,协程主动保存现场,运行时在条件满足后恢复它。 本文就从这三件事出发,一步步拆解协程是如何创建、暂停、调度和恢复的,并解释它为什么特别适合解决高并发 I/O 问题。

1. 协程不是轻量级线程

“轻量级线程”这个说法很方便,但不够准确。

线程由操作系统调度,拥有独立的调用栈,可以在任意指令附近被抢占。而协程通常由应用程序来调度,没有独立的传统线程栈,只会在约定的暂停点主动交出执行权。

更合适的理解方式是这样的:

  • 线程是执行代码的工人;
  • 协程是一张可以暂停的任务单;
  • 调度器决定哪个工人来处理哪张任务单。

一个线程可以先执行协程 A。A 在等待网络时暂停下来,这个线程马上就能转去执行协程 B。当数据到达后,A 被放回就绪队列,未来可能由原来的线程恢复,也可能由另一个线程接手。

所以,协程减少的不是计算量,而是等待所造成的线程占用。

2. 暂停之后,局部变量去了哪里

普通函数的状态主要保存在调用栈中。函数返回后,栈帧销毁,局部变量和执行位置也就跟着消失了。

但协程需要在暂停后保留这些信息。编译器会把协程函数改写成状态机,并创建一块协程帧,它通常包含:

  • 函数参数;
  • 那些跨越暂停点之后仍要使用的局部变量;
  • 当前执行到了哪个暂停点;
  • Promise 对象;
  • 恢复和销毁协程所需的管理信息。

举个例子:

 复制代码Task download() {
    std::string url = "https://example.com/data";
    auto body = co_await async_get(url);
    sa ve(body);
}

在等待 HTTP 响应的时候,url 之后还可能被用到,执行位置也必须被记住。它们都会随协程状态一起保存在协程帧中,而不是依赖当前线程的调用栈一直存在。

std::coroutine_handle 可以看作是指向协程帧的轻量级句柄:

 复制代码handle.resume();   // 从上次暂停的位置继续执行
handle.done();     // 查询协程是否已经结束
handle.destroy();  // 销毁协程帧

句柄通常只是一个指针大小的对象。真正需要认真对待的,是它背后那个协程帧的生命周期管理。

3. Promise:协程与外部世界的接口

C++20 没有提供一个现成的 Task 类型,而是提供了一套协议,让库作者来自定义协程的行为。只要一个函数使用了 co_await、co_yield 或 co_return,编译器就会通过返回类型找到对应的 promise_type。

下面是一个只用来解释结构的最小 Task:

 复制代码#include 
#include 
#include class Task {
public:
    struct promise_type {
        Task get_return_object() {
            return Task{
                std::coroutine_handle::from_promise(*this)
            };
        }        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() noexcept {}
        void unhandled_exception() { std::terminate(); }
    };    using Handle = std::coroutine_handle;    explicit Task(Handle h) : handle_(h) {}
    Task(Task&& other) noexcept
        : handle_(std::exchange(other.handle_, {})) {}
    Task(const Task&) = delete;    ~Task() {
        if (handle_) handle_.destroy();
    }    void resume() {
        if (handle_ && !handle_.done()) handle_.resume();
    }private:
    Handle handle_;
};

这几个函数控制了协程的关键时刻:

  • get_return_object():把新建的协程帧包装成一个用户可以持有的对象;
  • initial_suspend():决定函数创建后是立即运行,还是先暂停下来;
  • final_suspend():决定函数结束时如何交还控制权和回收资源;
  • return_void():处理没有返回值的 co_return;
  • unhandled_exception():处理从协程体里逃出来的异常。

示例中的 initial_suspend() 返回 suspend_always,意思是调用协程函数只会创建任务,而不会立即执行函数体。外部必须显式地调用 resume() 才行。这种行为通常被称为惰性启动。

final_suspend() 同样选择了暂停,因为“协程执行结束”和“协程帧可以销毁”并不是一回事。如果外部还持有句柄,协程帧一旦被立即释放,就会留下悬空指针。

4. co_await 到底调用了什么

co_await expression 最终会得到一个 Awaiter。Awaiter 通常提供三个方法:

 复制代码bool await_ready();
/* void、bool 或 coroutine_handle */ await_suspend(coroutine_handle h);
Result await_resume();

可以把它们理解成三个问题。

结果已经准备好了吗

await_ready() 返回 true 时,协程不会暂停,而是直接调用 await_resume()。这条路径被称为快路径。

比如说,异步读操作可以先尝试一次非阻塞读取。如果数据已经在内核缓冲区里了,那就完全没有必要经历那套“暂停—调度—恢复”的完整流程。

如果没有,谁来唤醒我

await_suspend() 会收到当前协程的句柄。它可以把句柄登记到定时器、I/O 事件循环、互斥锁等待队列或者消息通道中。

它的返回类型还会影响控制流:

  • 返回 void:当前协程确定暂停;
  • 返回 bool:true 表示暂停,false 表示取消暂停;
  • 返回另一个协程句柄:直接把控制权转移给那个协程,这叫作对称转移。

恢复后得到什么

协程被恢复时,会调用 await_resume()。它负责返回结果,或者在异步操作失败时抛出异常。

所以,co_await 不是“等待一下”的语法糖,而是一份明确的暂停协议:

 复制代码检查结果 -> 必要时登记当前协程 -> 交还线程
         -> 外部事件发生 -> 恢复协程 -> 读取结果

5. 用异步定时器看懂暂停与恢复

假设运行时提供了一个线程安全的定时器队列:

 复制代码class TimerQueue {
public:
    void add(std::chrono::steady_clock::time_point deadline,
             std::coroutine_handle<> handle);
};

我们可以为它编写一个 Awaiter:

 复制代码class SleepAwaiter {
public:
    SleepAwaiter(TimerQueue& timers,
                 std::chrono::milliseconds duration)
        : timers_(timers), duration_(duration) {}    bool await_ready() const noexcept {
        return duration_.count() <= 0;
    }    void await_suspend(std::coroutine_handle<> handle) {
        timers_.add(std::chrono::steady_clock::now() + duration_, handle);
    }    void await_resume() const noexcept {}private:
    TimerQueue& timers_;
    std::chrono::milliseconds duration_;
};

用户代码就可以写成这样:

 复制代码Task heartbeat(TimerQueue& timers) {
    while (true) {
        send_heartbeat();
        co_await SleepAwaiter{timers, std::chrono::seconds(1)};
    }
}

一次完整的暂停与恢复流程是这样的:

  1. await_ready() 判断出需要等待;
  2. await_suspend() 把当前协程的句柄和截止时间放进定时器队列;
  3. 协程暂停,执行它的线程可以腾出手来处理其他任务;
  4. 定时器线程发现时间到了;
  5. 运行时把协程句柄放入就绪队列;
  6. 某个工作线程取出这个句柄并调用 resume();
  7. await_resume() 返回,循环从 co_await 之后继续往下走。

这里需要提一点:定时器线程不一定要直接调用 resume()。把句柄放回一个统一的就绪队列,通常更容易控制并发、线程亲和性以及生命周期。

6. 调度器如何实现 M:N

光有暂停能力,还不足以构成一个异步运行时。系统还需要一个调度器,把 M 个协程安排到 N 个工作线程上:

 复制代码                   +----------------+
新任务 ----------> |    就绪队列     |
                   +-------+--------+
                           |
              +------------+------------+
              v            v            v
          Worker 1      Worker 2      Worker N
              ^            ^            ^
              +------------+------------+
                           |
                    I/O 或定时器就绪

一个最小调度循环的核心逻辑很简单:

 复制代码while (!stopped) {
    auto handle = ready_queue.pop();
    if (handle) {
        handle.resume();
    } else {
        wait_for_work();
    }
}

当然,实际实现要复杂得多。它通常还需要考虑:

  • 全局队列和线程本地队列;
  • 工作窃取,用于平衡不同 Worker 的负载;
  • 无任务时的休眠与唤醒,避免空转浪费 CPU;
  • 防止同一个协程被重复调度;
  • 任务取消、异常传播以及运行时的关闭协议。

M:N 并不表示 M 个任务就能同时并行执行。真正能同时运行的任务数量,仍然受 Worker 数量和 CPU 核心数限制。它解决的核心问题是:大量任务都可能存在,但只有那些真正就绪的任务才会占用线程。

还要注意,协程调度通常还是协作式的。如果一个协程持续进行计算,从不主动到达 co_await 或其他让出点,它就会长期占住 Worker。所以,CPU 密集型的任务需要主动分段、显式地让出执行权,或者交给专门的计算线程池来处理。

7. 真正困难的是生命周期

写一个能暂停的 Awaiter 并不难。真正困难的地方在于,保证它在所有情况下都只完成一次,并且在此之前,协程帧始终有效。

一个挂起的协程至少涉及三类所有权:

  • 谁拥有协程帧;
  • 谁拥有正在进行的异步操作;
  • 谁负责在成功、失败或取消时恢复协程。

如果协程帧先被销毁了,事件循环中保存的那个句柄就会变成悬空指针;如果两个事件都尝试恢复同一个协程,就会发生重复恢复;如果关闭连接时忘了通知等待者,协程就会永远停在暂停状态。

一个可靠的异步操作必须覆盖所有可能的出口:

  • 正常完成;
  • 超时;
  • 主动取消;
  • 对端关闭;
  • 系统调用失败;
  • 运行时整体关闭。

常见做法是为异步操作创建共享状态,并使用原子状态机来保证只有一个出口能赢得“完成权”。那些失败的竞争者只负责清理自己的事件,不再去触碰协程。

这里还有一条实用原则:在恢复协程之前,先把结果或错误完整地写入共享状态。 因为协程一旦恢复,就可能立即读取结果,甚至销毁整个异步操作对象。

8. 协程不会消除并发问题

同一个协程内部的代码看起来是顺序执行的,但两次恢复可能发生在不同的 Worker 上。所以,多个协程访问同一份数据时,仍然需要同步。

适合协程的同步原语,通常不会阻塞系统线程。比如,异步互斥锁在获取失败时,可以把协程句柄放进等待队列;解锁时,再调度队首的协程。消息通道在没有数据时暂停接收者,在缓冲区满时暂停发送者。

但“异步锁”并不是鼓励你在锁内部等待 I/O。持锁跨越一个不可控的 co_await,会放大竞争,也容易形成循环等待。更稳妥的做法是:

  1. 获取锁;
  2. 快速复制或更新共享状态;
  3. 释放锁;
  4. 然后再去执行那些可能长时间暂停的异步操作。

9. 初学者最容易踩的误区

co_await 不等于异步

如果 Awaiter 内部调用的是一个阻塞函数,那么线程照样会被阻塞。异步能力来自于非阻塞的系统调用、事件循环和正确的恢复协议,而不是来自于关键字本身。

协程不一定更快

协程擅长处理大量等待型的任务。纯计算任务并不会因为改写成协程就自动加速,反而可能增加状态管理和调度的额外成本。

暂停点同时也是并发边界

到达 co_await 之后,其他任务可能已经修改了共享状态;恢复也可能发生在另一个线程上。所以,跨暂停点保存引用、指针或迭代器时,需要重新检查对象的生命周期和并发条件。

不能只设计成功路径

网络断开、超时和取消不是边角情况,它们是异步系统的日常。每次保存协程句柄时,都应该同时考虑好,它在所有异常出口下如何被安全地唤醒和回收。

10. 阅读协程代码的三个问题

面对一段陌生的协程代码,不妨在每个 await_suspend() 处停下来,问自己三个问题:

  1. 协程帧由谁来保活?
  2. 哪个事件负责恢复它?
  3. 超时、取消和错误发生时,怎么收尾?

如果这三个问题都有明确且唯一的答案,那么暂停与恢复通常就是可靠的。如果其中有一个答案含糊不清,那么问题往往会在压力、断连或关闭阶段暴露出来。

结语

协程并没有让等待消失,它只是把等待这件事从线程手中拿走,交给了事件系统来管理。

编译器把函数变成一个可暂停的状态机,Awaiter 把协程句柄交给外部事件,调度器再把那些已经就绪的协程交给工作线程。看懂这条链路之后,co_await 就不再像魔法了,它不过是状态机、所有权和调度协议之间一次简洁的握手。

真正值得掌握的也不仅仅是协程语法,而是这条贯穿异步系统的原则:暂停必须有人保管,恢复必须有且只有一次,结束必须覆盖所有出口。

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
作者最新文章
编程开发
相关文章 更多
漫蛙漫画官方入口
漫蛙漫画官方入口

漫蛙漫画官方入口与核心功能解析 寻找一个稳定、资源丰富的漫画在线阅读平台,是许多漫画爱好者的共同需求。其中,漫蛙漫画官方入口及其免费全集服务,受到了不少关注。那么,这个平台究竟如何?下面,我们就来深入了解一下它的具体信息与特色功能。 平台的官方访问地址是:https://www.manwa123.c

漫蛙动漫APP最新版本下载
漫蛙动漫APP最新版本下载

对于动漫爱好者而言,找到一个资源全、体验佳的追番平台,常常是件头疼事。漫蛙动漫APP的定位很明确:就是要打造一个聚合全球正版动漫资源的专属乐园。无论是当季火爆的日漫新番、值得回味的经典国创,还是那些小众却精彩的优质动画,在这里基本都能一网打尽。平台底层采用了智能缓冲技术,最大程度保证了播放的流畅度,

漫蛙官网入口免费下载最新版
漫蛙官网入口免费下载最新版

想要获取正版manwa漫蛙APP?官方指定下载渠道就是漫蛙官网入口。 对于漫画迷而言,找到一款心仪的阅读神器至关重要。漫蛙APP正是这样一款专为全球漫画爱好者打造的平台,它汇聚了从日韩人气连载到本土原创精品的海量资源。说它“应有尽有”毫不为过。更重要的是,其界面设计清爽直观,搭配智能检索功能,能让用

漫蛙漫画在线观看入口
漫蛙漫画在线观看入口

想要畅享海量正版漫画资源?漫蛙漫画平台不容错过 对于漫画迷来说,找到一个资源丰富、体验流畅的正版平台实属幸事。漫蛙漫画凭借其优质的在线阅读服务,已经成为了许多资深读者的心头好。今天,我们就来详细拆解一下平台的访问方法与实用技巧,助你轻松开启漫画之旅。 漫蛙漫画网页入口页面指南 想立刻体验?官方在线观

漫蛙如何下载最新版本
漫蛙如何下载最新版本

漫蛙漫画网APP官方下载方法全解析 对于资深漫画迷来说,找到一个资源全、更新快、体验佳的阅读平台,就像淘到了宝藏。漫蛙漫画网APP,正是这样一个专为爱好者打造的精品集散地。它汇聚了全球优质的正版漫画,从火爆的日漫连载到崛起的国创佳作,平台都能做到实时同步更新。当然,第一步得先把它装到设备上。接下来,

漫蛙漫画APP最新版本下载
漫蛙漫画APP最新版本下载

漫蛙漫画APP:专业漫画阅读工具的全面革新 作为一款专业的漫画阅读工具,漫蛙漫画APP整合了全球优质漫画资源,其升级版漫蛙2在功能与体验上可谓全面革新。这款应用不仅囊括日漫经典与国漫新作,更通过简洁直观的界面设计和流畅操作,为用户打造沉浸式阅读环境,充分满足各类漫画爱好者的多样化需求。 漫蛙漫画特色

卖茶叶妈妈3d漫蛙安卓版下载
卖茶叶妈妈3d漫蛙安卓版下载

当下动漫圈热议的卖茶叶妈妈3d漫蛙下载,凭借其突破性的3D视觉技术成为焦点 最近动漫爱好者们都在讨论一个现象级产品——卖茶叶妈妈3d漫蛙下载。这款由漫蛙漫画平台倾力打造的3D作品专区,用立体建模与动态分镜彻底颠覆了传统二维漫画的阅读模式。你猜怎么着?现在只需通过专区提供的便捷下载通道,无论是热血冒险

漫蛙下载最新版本
漫蛙下载最新版本

漫蛙APP:资深漫画迷的移动图书馆与阅读利器 对于漫画爱好者来说,找到一个资源全、体验好的阅读平台,从来都不是件容易的事。而漫蛙APP,恰恰就是为这个需求而生的。它凭借海量的正版漫画库和极其便捷的离线下载功能,在圈内积累了相当不错的口碑。更重要的是,从经典的日本热血漫到新兴的国产原创作品,它几乎一网

漫蛙app下载安装最新版
漫蛙app下载安装最新版

专为漫画爱好者打造的漫蛙app:一款完全免费的阅读神器 对于漫画迷来说,找到一款资源全、体验好的阅读应用,常常不是件容易事。漫蛙app的出现,或许能打消这个顾虑。它主打完全免费,却汇集了海量国内外优质漫画资源。无论是追日韩的热门连载,还是淘本土的原创佳作,你都能在这里找到丰富的选择。应用的核心功夫下

漫蛙漫画APP下载安卓版免费
漫蛙漫画APP下载安卓版免费

漫蛙漫画APP:一款如何征服漫画爱好者的阅读利器 在众多漫画阅读平台中,漫蛙漫画APP免费版一直是许多深度爱好者的心头好。原因无他:它真正做到了将海内外精品漫画资源一网打尽。无论是历久弥新的日系经典,还是势头正劲的本土原创,在这里都能轻松找到。当然,光有海量内容还不够,应用的智能排版技术保证了画面的

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

PDF教程
PDF教程

正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

Mac软件 更多
IntelliJ IDEA macOS版
IntelliJ IDEA macOS版
Mac

IntelliJ IDEA 是 JetBrains 推出的专业集成开发环境,重点服务于 Java、Kotlin 与 JVM 项目开发。软件集成代码补全、静态检查、重构、调试、测试、版本控制及构建工具,并可借助插件扩展语言和框架支持。

PyCharm macOS版
PyCharm macOS版
Mac

JetBrains PyCharm 是面向 Python 开发者的集成开发环境,提供智能补全、代码检查、重构、调试、测试、版本控制及虚拟环境管理功能。Mac 版兼容 Intel 与 Apple 芯片,核心功能可免费使用。

WebStorm macOS版
WebStorm macOS版
Mac

JetBrains WebStorm 是面向 JavaScript、TypeScript 和现代 Web 技术的专业集成开发环境,可智能理解项目结构,提供代码补全、重构、错误检查、运行调试、测试及 Git 工具。

WINDOWS 更多
IntelliJ IDEA Windows版
IntelliJ IDEA Windows版
Windows

JetBrains IntelliJ IDEA 是以 Java、Kotlin 和 JVM 项目为核心的专业集成开发环境,提供智能补全、代码检查、安全重构、调试、测试、版本控制及构建工具集成。统一发行版的核心功能可免费用于商业和非商业项目,

PhpStorm Windows版
PhpStorm Windows版
Windows

PhpStorm 是 JetBrains 面向 PHP 与现代 Web 开发打造的专业集成开发环境,提供代码补全、静态检查、重构、调试、测试、数据库、版本控制及前端语言支持。Windows 用户可直接下载安装程序,

PyCharm Windows版
PyCharm Windows版
Windows

PyCharm 是 JetBrains 推出的 Python 集成开发环境,提供智能代码补全、错误检查、重构、调试、测试、Git、终端及 Jupyter Notebook 支持。统一版安装后可体验 30 天 Pro 功能,