C++20 协程从零入门:用 ASCII 图彻底理解 `co_await`、协程帧与异步执行

文章目录

  • [C++20 协程从零入门:用 ASCII 图彻底理解 `co_await`、协程帧与异步执行](#C++20 协程从零入门:用 ASCII 图彻底理解 co_await、协程帧与异步执行)
  • 一、协程到底解决了什么问题
  • 二、协程就是"可以暂停和恢复的函数"
  • 三、协程不是线程
    • [3.1 协程不会自动创建线程](#3.1 协程不会自动创建线程)
    • [3.2 谁调用 `resume()`,协程就在哪个线程恢复](#3.2 谁调用 resume(),协程就在哪个线程恢复)
  • [四、C++20 协程的三个关键字](#四、C++20 协程的三个关键字)
  • [五、协程为什么不能直接返回 `void`](#五、协程为什么不能直接返回 void)
  • 六、协程暂停以后,局部变量放在哪里
    • [6.1 普通函数的栈帧](#6.1 普通函数的栈帧)
    • [6.2 协程帧](#6.2 协程帧)
  • [七、`std::coroutine_handle` 是什么](#七、std::coroutine_handle 是什么)
  • 八、第一份可运行代码:手动暂停和恢复协程
    • [8.1 `task.hpp`](#8.1 task.hpp)
    • [8.2 `main.cpp`](#8.2 main.cpp)
    • [8.3 编译](#8.3 编译)
  • 九、第一份代码的完整执行图
  • [十、`initial_suspend()` 到底控制什么](#十、initial_suspend() 到底控制什么)
    • [10.1 返回 `std::suspend_never`](#10.1 返回 std::suspend_never)
    • [10.2 返回 `std::suspend_always`](#10.2 返回 std::suspend_always)
  • [十一、`final_suspend()` 为什么重要](#十一、final_suspend() 为什么重要)
    • [11.1 `final_suspend()` 返回 `suspend_always`](#11.1 final_suspend() 返回 suspend_always)
    • [11.2 错误组合](#11.2 错误组合)
  • [十二、`co_await` 到底做了什么](#十二、co_await 到底做了什么)
  • [十三、Awaiter 的三个核心函数](#十三、Awaiter 的三个核心函数)
    • [13.1 `await_ready()`](#13.1 await_ready())
    • [13.2 `await_suspend()`](#13.2 await_suspend())
    • [13.3 `await_resume()`](#13.3 await_resume())
  • [十四、自己实现一个最简单的 Awaiter](#十四、自己实现一个最简单的 Awaiter)
  • [十五、实现一个手动事件 `ManualEvent`](#十五、实现一个手动事件 ManualEvent)
    • [15.1 完整代码](#15.1 完整代码)
    • [15.2 执行图](#15.2 执行图)
  • [十六、这和真实异步网络 I/O 有什么关系](#十六、这和真实异步网络 I/O 有什么关系)
  • 十七、实现一个异步定时器
    • [17.1 为什么不能直接调用 `sleep_for`](#17.1 为什么不能直接调用 sleep_for)
    • [17.2 `DetachedTask`](#17.2 DetachedTask)
    • [17.3 `SleepFor` Awaiter](#17.3 SleepFor Awaiter)
    • [17.4 完整定时器程序](#17.4 完整定时器程序)
    • [17.5 定时器执行图](#17.5 定时器执行图)
  • [十八、为什么不能在 `main` 中固定睡眠四秒](#十八、为什么不能在 main 中固定睡眠四秒)
  • [十九、使用 `co_yield` 实现生成器](#十九、使用 co_yield 实现生成器)
    • [19.1 完整生成器代码](#19.1 完整生成器代码)
    • [19.2 `co_yield` 的转换过程](#19.2 co_yield 的转换过程)
  • 二十、协程中的局部变量与生命周期
    • [20.1 普通值通常可以跨越挂起点](#20.1 普通值通常可以跨越挂起点)
    • [20.2 引用不会自动延长对象生命周期](#20.2 引用不会自动延长对象生命周期)
    • [20.3 `this` 指针也可能悬空](#20.3 this 指针也可能悬空)
  • [二十一、协程 Lambda 为什么容易出现问题](#二十一、协程 Lambda 为什么容易出现问题)
  • 二十二、协程常见使用场景
    • [22.1 网络服务器](#22.1 网络服务器)
    • [22.2 定时任务](#22.2 定时任务)
    • [22.3 游戏脚本](#22.3 游戏脚本)
    • [22.4 惰性序列](#22.4 惰性序列)
  • 二十三、协程不能自动解决什么问题
    • [23.1 协程不会自动并行](#23.1 协程不会自动并行)
    • [23.2 协程不会把阻塞 I/O 自动变成异步 I/O](#23.2 协程不会把阻塞 I/O 自动变成异步 I/O)
  • 二十四、完整项目结构
  • 二十五、CMakeLists.txt
  • 二十六、学习协程时应该按什么顺序
  • 二十七、最后建立完整思维模型

C++20 协程从零入门:用 ASCII 图彻底理解 co_await、协程帧与异步执行

适合读者:

会写普通 C++ 函数,但第一次接触 C++20 协程。

本文不要求你提前了解 promise、状态机或异步框架。

我们会从"普通函数为什么不能暂停"开始,一步一步写出可以编译运行的协程程序。


一、协程到底解决了什么问题

先看一个普通函数:

cpp 复制代码
#include <iostream>

void normal_function()
{
    std::cout << "步骤 1:开始执行\n";
    std::cout << "步骤 2:处理中\n";
    std::cout << "步骤 3:执行结束\n";
}

int main()
{
    std::cout << "main:调用普通函数\n";

    normal_function();

    std::cout << "main:普通函数返回\n";
}

普通函数的执行流程是:

text 复制代码
main()
  │
  │ 调用 normal_function()
  ▼
normal_function()
  │
  ├── 执行步骤 1
  ├── 执行步骤 2
  ├── 执行步骤 3
  │
  ▼
函数结束,返回 main()
  │
  ▼
main() 继续执行

普通函数具有一个非常重要的特点:

text 复制代码
调用者
  │
  │ 调用函数
  ▼
被调用函数必须执行完毕
  │
  │ return
  ▼
调用者才能继续执行

普通函数不能在中间说:

text 复制代码
我先停在这里。

你先去执行其他代码。

以后再回来从这里继续执行。

而这正是协程能够做到的事情。


二、协程就是"可以暂停和恢复的函数"

协程的执行流程可以是:

text 复制代码
main()
  │
  │ 调用协程
  ▼
协程开始执行
  │
  ├── 执行步骤 1
  ├── 执行步骤 2
  │
  ├── 遇到 co_await
  │
  ▼
协程暂停
  │
  └──────────────────────┐
                         │
main() 继续执行           │
  │                      │
  ├── 做其他事情          │
  │                      │
  ├── 调用 resume() ──────┘
  ▼
协程从 co_await 后继续执行
  │
  ├── 执行步骤 3
  │
  ▼
协程结束

所以,可以把协程理解为:

text 复制代码
协程 = 可以暂停 + 可以保存状态 + 可以恢复的函数

三、协程不是线程

这是初学协程时最容易出现的误解。

3.1 协程不会自动创建线程

下面的协程:

cpp 复制代码
Task test()
{
    std::cout << "协程开始\n";

    co_await std::suspend_always{};

    std::cout << "协程恢复\n";
}

本身不会创建任何新线程。

它只做两件事:

text 复制代码
1. 执行到 co_await 时暂停
2. 将来有人调用 resume() 时继续执行

3.2 谁调用 resume(),协程就在哪个线程恢复

假设主线程调用:

cpp 复制代码
handle.resume();

那么协程就在主线程执行。

text 复制代码
主线程
  │
  ├── handle.resume()
  │
  ▼
协程在主线程继续执行

如果工作线程调用:

cpp 复制代码
handle.resume();

那么协程就在工作线程中恢复。

text 复制代码
工作线程
  │
  ├── handle.resume()
  │
  ▼
协程在工作线程继续执行

因此:

text 复制代码
协程负责:暂停、保存状态、恢复

线程负责:真正运行代码

事件循环负责:决定什么时候恢复协程

四、C++20 协程的三个关键字

C++20 提供了三个协程关键字:

关键字 作用
co_await 等待某个操作,必要时暂停当前协程
co_return 结束协程,或者返回协程结果
co_yield 产生一个值,然后暂停协程

例如:

cpp 复制代码
Task example()
{
    // 等待某个操作。
    co_await some_operation();

    // 结束协程。
    co_return;
}

只要函数体中出现以下任意一个关键字:

cpp 复制代码
co_await
co_return
co_yield

编译器就可能把这个函数转换成协程。


五、协程为什么不能直接返回 void

初学者可能会这样写:

cpp 复制代码
void test()
{
    co_await std::suspend_always{};
}

这通常不能编译。

原因是协程执行到一半可以暂停,所以调用者必须能够获得一个对象,用来管理协程。

这个对象通常叫:

text 复制代码
Task
Generator
Awaitable
AsyncTask

例如:

cpp 复制代码
Task test()
{
    co_await std::suspend_always{};
}

调用者拿到 Task 后,可以通过它:

text 复制代码
恢复协程
判断协程是否结束
销毁协程帧
取得返回值
处理异常

六、协程暂停以后,局部变量放在哪里

考虑下面的代码:

cpp 复制代码
Task test()
{
    int number = 100;

    co_await std::suspend_always{};

    std::cout << number << '\n';
}

问题是:

text 复制代码
协程执行到 co_await 后,函数暂时返回了。

但是以后恢复时,number 为什么还存在?

答案是:

编译器会为协程创建一块特殊内存,叫作协程帧。

英文名称:

text 复制代码
Coroutine Frame

6.1 普通函数的栈帧

普通函数调用时,局部变量通常保存在函数栈帧中:

text 复制代码
线程栈
高地址
┌────────────────────────┐
│ main() 的局部变量       │
├────────────────────────┤
│ test() 的局部变量       │
│ number = 100            │
├────────────────────────┤
│ 返回地址                │
└────────────────────────┘
低地址

test() 返回后,它的栈帧通常失效:

text 复制代码
test() 返回
    │
    ▼
test() 的普通局部变量生命周期结束

6.2 协程帧

协程可能在函数"返回"以后继续执行,因此跨越挂起点的数据需要被保存。

编译器会生成类似下面的结构:

text 复制代码
协程帧 Coroutine Frame
┌────────────────────────────────┐
│ promise_type 对象              │
├────────────────────────────────┤
│ 协程函数参数                   │
├────────────────────────────────┤
│ 跨越挂起点的局部变量           │
│ number = 100                   │
├────────────────────────────────┤
│ 当前执行位置                   │
│ 例如:暂停在第一个 co_await    │
├────────────────────────────────┤
│ 当前 Awaiter 对象              │
├────────────────────────────────┤
│ 异常状态、返回值等             │
└────────────────────────────────┘

协程暂停后:

text 复制代码
普通函数调用暂时返回
        │
        ▼
协程帧仍然存在
        │
        ├── 局部变量仍然存在
        ├── 当前执行位置仍然存在
        └── promise_type 仍然存在

恢复协程时:

text 复制代码
coroutine_handle
        │
        ▼
找到协程帧
        │
        ▼
从保存的执行位置继续执行

七、std::coroutine_handle 是什么

协程句柄可以理解为:

指向协程帧的控制器。

可以把关系画成:

text 复制代码
Task 对象
┌──────────────────────────┐
│ coroutine_handle handle  │
└─────────────┬────────────┘
              │
              │ 指向
              ▼
协程帧
┌──────────────────────────┐
│ promise_type             │
│ 局部变量                 │
│ 参数                     │
│ 当前执行位置             │
└──────────────────────────┘

通过句柄可以进行三个常见操作:

cpp 复制代码
handle.resume();   // 恢复协程
handle.done();     // 判断协程是否已经结束
handle.destroy();  // 销毁协程帧

可以把它们理解为:

text 复制代码
resume()  :继续运行
done()    :查询是否执行完
destroy() :释放协程帧内存

八、第一份可运行代码:手动暂停和恢复协程

首先实现一个最小的 Task

项目结构:

text 复制代码
cpp20_coroutine/
├── task.hpp
└── main.cpp

8.1 task.hpp

cpp 复制代码
#pragma once

#include <coroutine>
#include <exception>
#include <utility>

// Task 表示一个"无返回值的协程任务"。
//
// 它主要负责:
// 1. 保存协程句柄。
// 2. 恢复协程。
// 3. 判断协程是否结束。
// 4. 在析构时销毁协程帧。
// 5. 保存并重新抛出协程中的异常。
class Task
{
public:
    // 先声明 promise_type。
    //
    // 编译器发现某个协程函数返回 Task 时,
    // 会查找 Task::promise_type。
    struct promise_type;

    // handle_type 是一个"带 promise_type 类型信息"的协程句柄。
    //
    // 它不仅能恢复和销毁协程,
    // 还可以通过 promise() 访问协程帧中的 promise_type。
    using handle_type = std::coroutine_handle<promise_type>;

public:
    // Task 由协程句柄构造。
    explicit Task(handle_type handle) noexcept
        : handle_(handle)
    {
    }

    // Task 负责协程帧的所有权。
    //
    // 当 Task 被销毁时,也要销毁协程帧。
    ~Task()
    {
        if (handle_)
        {
            handle_.destroy();
        }
    }

    // 禁止拷贝。
    //
    // 原因:
    // 如果两个 Task 保存同一个协程句柄,
    // 那么两个 Task 析构时都会调用 destroy(),
    // 导致重复释放协程帧。
    Task(const Task&) = delete;
    Task& operator=(const Task&) = delete;

    // 允许移动构造。
    //
    // 移动后,协程句柄的所有权转移给新对象。
    Task(Task&& other) noexcept
        : handle_(std::exchange(other.handle_, {}))
    {
    }

    // 允许移动赋值。
    Task& operator=(Task&& other) noexcept
    {
        if (this != &other)
        {
            // 当前 Task 可能已经管理一个协程帧,
            // 需要先销毁原来的协程帧。
            if (handle_)
            {
                handle_.destroy();
            }

            // 接管 other 的协程句柄。
            handle_ = std::exchange(other.handle_, {});
        }

        return *this;
    }

    // 判断协程是否执行结束。
    [[nodiscard]] bool done() const noexcept
    {
        // 没有句柄,也可以认为任务不可再执行。
        return !handle_ || handle_.done();
    }

    // 恢复协程。
    //
    // 返回值:
    // true  :恢复后协程仍未结束。
    // false :协程不存在,或者已经执行结束。
    bool resume()
    {
        // 不能恢复空句柄。
        if (!handle_)
        {
            return false;
        }

        // 已经执行结束的协程不能再次 resume。
        if (handle_.done())
        {
            return false;
        }

        // 从上一次挂起点继续执行。
        handle_.resume();

        // 如果协程已经执行结束,并且其中保存了异常,
        // 就把异常重新抛到调用 resume() 的位置。
        if (handle_.done() &&
            handle_.promise().exception_)
        {
            std::rethrow_exception(
                handle_.promise().exception_
            );
        }

        return !handle_.done();
    }

public:
    // promise_type 是编译器与 Task 之间的接口。
    //
    // 编译器通过它决定:
    // 1. 协程返回什么对象。
    // 2. 协程开始前是否挂起。
    // 3. 协程结束后是否挂起。
    // 4. co_return 如何处理。
    // 5. 异常如何处理。
    struct promise_type
    {
        // 保存协程中没有被捕获的异常。
        std::exception_ptr exception_;

        // 创建调用者最终拿到的 Task 对象。
        Task get_return_object() noexcept
        {
            // 当前 promise_type 位于协程帧中。
            //
            // from_promise(*this) 可以通过 promise_type
            // 找到它所属的协程帧,并创建协程句柄。
            return Task{
                handle_type::from_promise(*this)
            };
        }

        // initial_suspend 决定:
        // 协程创建后,是否在进入函数体之前先暂停。
        //
        // suspend_never 表示"不暂停",立即执行协程函数体。
        std::suspend_never initial_suspend() const noexcept
        {
            return {};
        }

        // final_suspend 决定:
        // 协程函数体执行结束后,是否保留协程帧。
        //
        // suspend_always 表示协程结束后先挂起,
        // 不立即自动销毁协程帧。
        //
        // 最后由 Task 析构函数调用 handle.destroy()。
        std::suspend_always final_suspend() const noexcept
        {
            return {};
        }

        // 处理无返回值的 co_return。
        //
        // 对于下面的代码:
        //
        // co_return;
        //
        // 编译器最终会调用 return_void()。
        void return_void() const noexcept
        {
        }

        // 处理协程中未捕获的异常。
        void unhandled_exception() noexcept
        {
            exception_ = std::current_exception();
        }
    };

private:
    // Task 持有的协程句柄。
    handle_type handle_{};
};

8.2 main.cpp

cpp 复制代码
#include "task.hpp"

#include <coroutine>
#include <iostream>

// 只要函数体中出现 co_await、co_return 或 co_yield,
// 这个函数就可能被编译器转换成协程。
Task basic_coroutine()
{
    std::cout << "2. 协程:开始执行\n";

    int number = 100;

    std::cout << "3. 协程:number = "
              << number
              << '\n';

    std::cout << "4. 协程:即将执行 co_await\n";

    // std::suspend_always 是标准库提供的等待器。
    //
    // 它的含义是:
    // 执行到这里时,协程一定挂起。
    //
    // 协程暂停后,控制权回到 main。
    co_await std::suspend_always{};

    // 只有外部调用 resume() 后,才会执行到这里。
    std::cout << "7. 协程:已经恢复执行\n";

    // number 跨越了挂起点。
    //
    // 它通常保存在协程帧中,
    // 所以恢复后仍然可以正常访问。
    std::cout << "8. 协程:number 仍然等于 "
              << number
              << '\n';

    // 无返回值地结束协程。
    co_return;
}

int main()
{
    std::cout << "1. main:准备调用协程\n";

    // 调用 basic_coroutine() 时:
    //
    // 1. 创建协程帧。
    // 2. 创建 promise_type。
    // 3. 创建 Task。
    // 4. initial_suspend() 返回 suspend_never。
    // 5. 立即进入协程函数体。
    // 6. 一直执行到第一个真正的挂起点。
    Task task = basic_coroutine();

    // 协程执行到 co_await suspend_always 后暂停,
    // 所以程序回到 main。
    std::cout << "5. main:重新获得控制权\n";

    std::cout << "6. main:调用 task.resume()\n";

    // 恢复协程。
    task.resume();

    std::cout << "9. main:协程是否已经结束:"
              << std::boolalpha
              << task.done()
              << '\n';

    // main 结束时,task 被析构。
    //
    // Task::~Task() 会执行 handle.destroy(),
    // 释放协程帧。
    return 0;
}

8.3 编译

GCC:

bash 复制代码
g++ -std=c++20 -Wall -Wextra -pedantic \
    main.cpp -o coroutine_demo

运行:

bash 复制代码
./coroutine_demo

可能输出:

text 复制代码
1. main:准备调用协程
2. 协程:开始执行
3. 协程:number = 100
4. 协程:即将执行 co_await
5. main:重新获得控制权
6. main:调用 task.resume()
7. 协程:已经恢复执行
8. 协程:number 仍然等于 100
9. main:协程是否已经结束:true

九、第一份代码的完整执行图

调用:

cpp 复制代码
Task task = basic_coroutine();

编译器大致执行:

text 复制代码
main()
  │
  │ 调用 basic_coroutine()
  ▼
创建协程帧
┌─────────────────────────────┐
│ promise_type                │
│ number                      │
│ 当前执行位置                │
│ 其他协程状态                │
└─────────────────────────────┘
  │
  ▼
调用 promise.get_return_object()
  │
  ▼
创建 Task
  │
  ▼
调用 initial_suspend()
  │
  ├── 返回 suspend_never
  │
  ▼
立即进入协程函数体

执行到:

cpp 复制代码
co_await std::suspend_always{};

流程变成:

text 复制代码
basic_coroutine()
  │
  ├── number = 100
  │
  ├── 执行 co_await
  │
  ▼
协程暂停
  │
  ├── 保存当前执行位置
  ├── 保存 number
  └── 保留协程帧
  │
  ▼
控制权返回 main()

调用:

cpp 复制代码
task.resume();

流程为:

text 复制代码
main()
  │
  ├── task.resume()
  │
  ▼
Task::resume()
  │
  ├── handle_.resume()
  │
  ▼
通过句柄找到协程帧
  │
  ▼
从 co_await 后继续执行
  │
  ├── 输出 number
  ├── 执行 co_return
  │
  ▼
调用 return_void()
  │
  ▼
调用 final_suspend()
  │
  ├── 返回 suspend_always
  │
  ▼
协程结束,但协程帧暂时保留

最后:

text 复制代码
main() 结束
  │
  ▼
Task 析构
  │
  ├── handle.destroy()
  │
  ▼
协程帧被释放

十、initial_suspend() 到底控制什么

initial_suspend() 控制:

text 复制代码
协程创建后,要不要在进入函数体之前先暂停?

10.1 返回 std::suspend_never

cpp 复制代码
std::suspend_never initial_suspend() noexcept
{
    return {};
}

执行过程:

text 复制代码
调用协程函数
    │
    ▼
创建协程帧
    │
    ▼
不暂停
    │
    ▼
立即进入协程函数体

这种模式通常叫:

text 复制代码
Eager Coroutine
急切启动协程

调用协程后,协程会立即开始执行。


10.2 返回 std::suspend_always

cpp 复制代码
std::suspend_always initial_suspend() noexcept
{
    return {};
}

执行过程:

text 复制代码
调用协程函数
    │
    ▼
创建协程帧
    │
    ▼
在函数体执行前暂停
    │
    ▼
返回 Task 给调用者

此时协程函数体一行都没有执行。

只有调用:

cpp 复制代码
task.resume();

才会真正进入函数体。

这种模式通常叫:

text 复制代码
Lazy Coroutine
惰性启动协程

十一、final_suspend() 为什么重要

final_suspend() 控制:

text 复制代码
协程函数体结束后,协程帧是否立即自动销毁。

11.1 final_suspend() 返回 suspend_always

cpp 复制代码
std::suspend_always final_suspend() noexcept
{
    return {};
}

执行流程:

text 复制代码
协程函数体结束
    │
    ▼
进入 final_suspend
    │
    ▼
暂停在最终挂起点
    │
    ▼
协程帧继续保留
    │
    ▼
以后由 Task 调用 destroy()

这种情况下:

cpp 复制代码
~Task()
{
    handle.destroy();
}

是合理的。


11.2 错误组合

下面的写法非常危险:

cpp 复制代码
std::suspend_never final_suspend() noexcept
{
    return {};
}

~Task()
{
    handle.destroy();
}

因为:

text 复制代码
协程函数体结束
    │
    ▼
final_suspend 返回 suspend_never
    │
    ▼
协程帧可能已经自动销毁
    │
    ▼
Task 析构再次调用 destroy()
    │
    ▼
重复销毁
    │
    ▼
未定义行为

所以需要记住:

text 复制代码
Task 持有句柄并负责 destroy()
          │
          ▼
final_suspend 通常使用 suspend_always

十二、co_await 到底做了什么

下面是整个协程机制中最重要的一部分。

代码:

cpp 复制代码
int value = co_await operation;

大致可以理解为:

cpp 复制代码
auto awaiter = 获取 operation 对应的 Awaiter;

if (!awaiter.await_ready())
{
    awaiter.await_suspend(当前协程句柄);

    // 当前协程可能在这里暂停。
}

// 协程恢复后执行。
int value = awaiter.await_resume();

注意,这只是帮助理解的伪代码,不是标准规定的完整编译器转换结果。


十三、Awaiter 的三个核心函数

一个典型 Awaiter 包含:

cpp 复制代码
struct Awaiter
{
    bool await_ready();

    void await_suspend(
        std::coroutine_handle<> handle
    );

    int await_resume();
};

执行顺序为:

text 复制代码
co_await operation
        │
        ▼
获取 Awaiter
        │
        ▼
await_ready()
        │
        ├── true ──────────────┐
        │                      │
        └── false              │
              │                │
              ▼                │
       await_suspend(handle)   │
              │                │
              ▼                │
          协程暂停              │
              │                │
          外部恢复协程           │
              │                │
              └────────────────┤
                               ▼
                        await_resume()
                               │
                               ▼
                        得到 co_await 结果

13.1 await_ready()

cpp 复制代码
bool await_ready();

它负责回答:

text 复制代码
等待的操作是不是已经完成了?

返回 true

text 复制代码
操作已经完成
不需要暂停
直接执行 await_resume()

返回 false

text 复制代码
操作还没有完成
执行 await_suspend()
协程可能暂停

13.2 await_suspend()

cpp 复制代码
void await_suspend(
    std::coroutine_handle<> handle
);

参数 handle 是:

text 复制代码
当前正在执行 co_await 的协程句柄

Awaiter 可以保存这个句柄:

cpp 复制代码
saved_handle = handle;

等异步操作完成以后,再执行:

cpp 复制代码
saved_handle.resume();

13.3 await_resume()

cpp 复制代码
int await_resume();

协程恢复后会调用它。

它通常负责:

text 复制代码
取得结果
返回结果
检查错误
抛出异常
执行恢复后的清理

例如:

cpp 复制代码
int await_resume()
{
    return result_;
}

那么:

cpp 复制代码
int value = co_await operation;

中的 value 最终就来自:

cpp 复制代码
awaiter.await_resume();

十四、自己实现一个最简单的 Awaiter

先实现一个只负责打印流程的等待器。

cpp 复制代码
#include "task.hpp"

#include <coroutine>
#include <iostream>

struct DebugAwaiter
{
    // 第一步:检查操作是否已经完成。
    bool await_ready() const noexcept
    {
        std::cout
            << "DebugAwaiter::await_ready()\n";

        // 返回 false,表示操作尚未完成,
        // 协程需要进入 await_suspend。
        return false;
    }

    // 第二步:协程即将暂停。
    void await_suspend(
        std::coroutine_handle<> handle
    ) const noexcept
    {
        std::cout
            << "DebugAwaiter::await_suspend()\n";

        // 这个示例中不保存 handle,
        // 所以外部不能通过 Awaiter 自动恢复协程。
        //
        // 协程之后需要通过 Task::resume() 手动恢复。
        (void)handle;
    }

    // 第三步:协程恢复后调用。
    int await_resume() const noexcept
    {
        std::cout
            << "DebugAwaiter::await_resume()\n";

        // co_await 表达式的结果就是这个返回值。
        return 123;
    }
};

Task awaiter_demo()
{
    std::cout << "1. 协程:执行 co_await 前\n";

    int result = co_await DebugAwaiter{};

    std::cout << "5. 协程:co_await 返回结果 = "
              << result
              << '\n';
}

int main()
{
    Task task = awaiter_demo();

    std::cout << "4. main:准备恢复协程\n";

    task.resume();
}

输出顺序:

text 复制代码
1. 协程:执行 co_await 前
DebugAwaiter::await_ready()
DebugAwaiter::await_suspend()
2. main:准备恢复协程
DebugAwaiter::await_resume()
3. 协程:co_await 返回结果 = 123

完整流程:

text 复制代码
协程执行 co_await DebugAwaiter{}
             │
             ▼
调用 await_ready()
             │
             ├── 返回 false
             │
             ▼
调用 await_suspend(handle)
             │
             ▼
协程暂停,main 继续执行
             │
             ▼
main 调用 task.resume()
             │
             ▼
协程恢复
             │
             ▼
调用 await_resume()
             │
             ├── 返回 123
             │
             ▼
result = 123

十五、实现一个手动事件 ManualEvent

现在实现一个更接近真实异步模型的例子。

目标:

text 复制代码
协程等待事件
    │
    ▼
协程暂停
    │
    ▼
main 触发事件
    │
    ▼
事件自动恢复协程

15.1 完整代码

cpp 复制代码
#include "task.hpp"

#include <coroutine>
#include <iostream>
#include <utility>

class ManualEvent
{
public:
    // Awaiter 是真正参与 co_await 协议的对象。
    class Awaiter
    {
    public:
        explicit Awaiter(
            ManualEvent& event
        ) noexcept
            : event_(event)
        {
        }

        // 如果事件已经触发,则不需要暂停协程。
        bool await_ready() const noexcept
        {
            return event_.signaled_;
        }

        // 如果事件还没有触发,
        // 就保存当前协程句柄。
        void await_suspend(
            std::coroutine_handle<> handle
        ) noexcept
        {
            event_.waiter_ = handle;
        }

        // 这个事件没有返回值,
        // 所以 await_resume 返回 void。
        void await_resume() const noexcept
        {
        }

    private:
        ManualEvent& event_;
    };

public:
    // 当代码写:
    //
    // co_await event;
    //
    // 编译器可以通过 operator co_await()
    // 得到真正的 Awaiter。
    Awaiter operator co_await() noexcept
    {
        return Awaiter{*this};
    }

    // 触发事件。
    void set()
    {
        signaled_ = true;

        if (waiter_)
        {
            // 先取出并清空 waiter_。
            //
            // 这样可以避免恢复协程后,
            // 事件对象中继续保留旧句柄。
            auto handle =
                std::exchange(waiter_, {});

            // 恢复等待此事件的协程。
            handle.resume();
        }
    }

private:
    // 事件是否已经触发。
    bool signaled_ = false;

    // 正在等待该事件的协程句柄。
    //
    // 这个简单版本只支持一个等待者。
    std::coroutine_handle<> waiter_{};
};

Task wait_for_event(ManualEvent& event)
{
    std::cout << "1. 协程:开始等待事件\n";

    // 如果 event 尚未 set(),
    // 协程会在这里暂停。
    co_await event;

    // event.set() 恢复协程后,
    // 才会执行到这里。
    std::cout << "4. 协程:事件已经触发\n";
}

int main()
{
    ManualEvent event;

    // 协程立即启动。
    //
    // 它执行到 co_await event 时暂停。
    Task task = wait_for_event(event);

    std::cout << "2. main:协程已经挂起\n";

    std::cout << "3. main:调用 event.set()\n";

    // set() 内部会调用协程句柄的 resume()。
    event.set();

    std::cout << "5. main:协程是否结束 = "
              << std::boolalpha
              << task.done()
              << '\n';
}

15.2 执行图

text 复制代码
main()
  │
  ├── 创建 ManualEvent event
  │
  ├── 调用 wait_for_event(event)
  │
  ▼
协程开始执行
  │
  ├── 输出"开始等待事件"
  │
  ├── co_await event
  │
  ▼
event.operator co_await()
  │
  ▼
创建 ManualEvent::Awaiter
  │
  ▼
Awaiter::await_ready()
  │
  ├── event.signaled_ == false
  │
  └── 返回 false
  │
  ▼
Awaiter::await_suspend(handle)
  │
  ├── event.waiter_ = handle
  │
  ▼
协程暂停
  │
  ▼
main() 继续执行
  │
  ├── event.set()
  │
  ▼
ManualEvent::set()
  │
  ├── signaled_ = true
  ├── 取出 waiter_
  ├── waiter_.resume()
  │
  ▼
协程恢复
  │
  ├── await_resume()
  ├── 执行 co_await 后面的代码
  │
  ▼
协程结束

十六、这和真实异步网络 I/O 有什么关系

真实网络异步读取大致是:

cpp 复制代码
std::string data =
    co_await socket.async_read();

内部可能执行:

text 复制代码
协程调用 async_read()
        │
        ▼
检查 Socket 是否已经有数据
        │
        ├── 有数据
        │     │
        │     ▼
        │  直接返回数据
        │
        └── 没有数据
              │
              ▼
      把 Socket 注册到 epoll
              │
              ▼
      保存当前协程句柄
              │
              ▼
          协程暂停
              │
              ▼
       epoll_wait 等待事件
              │
              ▼
        Socket 变为可读
              │
              ▼
      事件循环调用 handle.resume()
              │
              ▼
          协程恢复
              │
              ▼
       await_resume() 读取数据
              │
              ▼
     co_await 表达式得到结果

所以,协程并没有取代 epoll

它只是把原本的回调写法:

cpp 复制代码
async_read(socket, [](std::string data) {
    async_write(socket, data, [](bool success) {
        // 继续处理。
    });
});

改造成更接近同步代码的写法:

cpp 复制代码
Task handle_client(Socket socket)
{
    std::string data =
        co_await socket.async_read();

    co_await socket.async_write(data);
}

底层仍然需要:

text 复制代码
epoll
IOCP
线程池
事件循环
操作系统异步接口

十七、实现一个异步定时器

下面使用工作线程模拟异步定时器。

目标:

cpp 复制代码
co_await SleepFor{1000ms};

执行效果:

text 复制代码
协程暂停一秒
主线程不会被阻塞
一秒后工作线程恢复协程

17.1 为什么不能直接调用 sleep_for

下面的代码虽然是协程,但依旧会阻塞当前线程:

cpp 复制代码
Task wrong_sleep()
{
    std::this_thread::sleep_for(
        std::chrono::seconds(1)
    );

    co_return;
}

流程是:

text 复制代码
当前线程
  │
  ├── 调用 sleep_for()
  │
  ▼
当前线程真的睡眠一秒
  │
  ▼
一秒后继续执行

协程关键字不会把普通阻塞函数自动转换成异步函数。


17.2 DetachedTask

异步定时器示例使用一个自动销毁的协程类型:

cpp 复制代码
#include <coroutine>
#include <exception>

struct DetachedTask
{
    struct promise_type
    {
        // 调用者不需要保存任何返回对象。
        DetachedTask get_return_object() const noexcept
        {
            return {};
        }

        // 创建后立即执行协程函数体。
        std::suspend_never initial_suspend() const noexcept
        {
            return {};
        }

        // 协程结束后不保留协程帧。
        //
        // 协程帧会自动销毁。
        std::suspend_never final_suspend() const noexcept
        {
            return {};
        }

        // 处理无返回值 co_return。
        void return_void() const noexcept
        {
        }

        // 这个简单示例遇到异常时直接终止程序。
        void unhandled_exception() const noexcept
        {
            std::terminate();
        }
    };
};

它的生命周期模式是:

text 复制代码
DetachedTask 不保存协程句柄
          │
          ▼
final_suspend 返回 suspend_never
          │
          ▼
协程执行结束后自动销毁协程帧

17.3 SleepFor Awaiter

cpp 复制代码
#include <chrono>
#include <coroutine>
#include <thread>

class SleepFor
{
public:
    explicit SleepFor(
        std::chrono::milliseconds duration
    ) noexcept
        : duration_(duration)
    {
    }

    // 如果等待时间小于或等于 0,
    // 可以认为操作已经完成,不需要挂起。
    bool await_ready() const noexcept
    {
        return duration_.count() <= 0;
    }

    // 保存协程句柄,并创建工作线程。
    void await_suspend(
        std::coroutine_handle<> handle
    ) const
    {
        const auto duration = duration_;

        std::thread(
            [duration, handle]() mutable
            {
                // 工作线程负责等待。
                std::this_thread::sleep_for(duration);

                // 等待结束后,由工作线程恢复协程。
                handle.resume();
            }
        ).detach();
    }

    // 定时器没有需要返回的数据。
    void await_resume() const noexcept
    {
    }

private:
    std::chrono::milliseconds duration_;
};

17.4 完整定时器程序

cpp 复制代码
#include <chrono>
#include <coroutine>
#include <exception>
#include <iostream>
#include <latch>
#include <thread>

using namespace std::chrono_literals;

struct DetachedTask
{
    struct promise_type
    {
        DetachedTask get_return_object() const noexcept
        {
            return {};
        }

        std::suspend_never initial_suspend() const noexcept
        {
            return {};
        }

        std::suspend_never final_suspend() const noexcept
        {
            return {};
        }

        void return_void() const noexcept
        {
        }

        void unhandled_exception() const noexcept
        {
            std::terminate();
        }
    };
};

class SleepFor
{
public:
    explicit SleepFor(
        std::chrono::milliseconds duration
    ) noexcept
        : duration_(duration)
    {
    }

    bool await_ready() const noexcept
    {
        return duration_.count() <= 0;
    }

    void await_suspend(
        std::coroutine_handle<> handle
    ) const
    {
        const auto duration = duration_;

        std::thread(
            [duration, handle]() mutable
            {
                std::this_thread::sleep_for(duration);

                handle.resume();
            }
        ).detach();
    }

    void await_resume() const noexcept
    {
    }

private:
    std::chrono::milliseconds duration_;
};

DetachedTask timer_demo(std::latch& finished)
{
    std::cout
        << "协程开始,当前线程 ID = "
        << std::this_thread::get_id()
        << '\n';

    // 协程在这里暂停。
    //
    // SleepFor 会启动工作线程,
    // 工作线程一秒后调用 handle.resume()。
    co_await SleepFor{1000ms};

    std::cout
        << "第一次等待结束,当前线程 ID = "
        << std::this_thread::get_id()
        << '\n';

    co_await SleepFor{1000ms};

    std::cout
        << "第二次等待结束,当前线程 ID = "
        << std::this_thread::get_id()
        << '\n';

    // 通知 main:
    // 整个协程已经执行完毕。
    finished.count_down();
}

int main()
{
    // 初始计数为 1。
    //
    // 只有协程调用 count_down() 后,
    // main 中的 wait() 才会结束。
    std::latch finished{1};

    std::cout
        << "main 线程 ID = "
        << std::this_thread::get_id()
        << '\n';

    // timer_demo 是立即启动的协程。
    timer_demo(finished);

    // 协程已经在第一个 co_await 处暂停,
    // 所以 main 可以继续执行。
    std::cout
        << "main:协程等待期间,"
        << "我仍然可以继续执行\n";

    // 等待协程真正完成。
    finished.wait();

    std::cout << "main:协程已经执行结束\n";
}

编译:

bash 复制代码
g++ -std=c++20 -Wall -Wextra -pedantic \
    -pthread async_timer.cpp \
    -o async_timer

17.5 定时器执行图

text 复制代码
主线程
  │
  ├── 调用 timer_demo()
  │
  ▼
协程开始执行
  │
  ├── 输出线程 ID
  ├── co_await SleepFor{1000ms}
  │
  ▼
SleepFor::await_ready()
  │
  ├── 返回 false
  │
  ▼
SleepFor::await_suspend(handle)
  │
  ├── 创建工作线程 A
  │
  ▼
协程暂停
  │
  ▼
主线程继续执行
  │
  ├── 输出"可以继续执行"
  ├── finished.wait()
  │
  ▼
主线程等待完成信号

工作线程 A:

text 复制代码
工作线程 A
  │
  ├── sleep_for(1 秒)
  │
  ▼
调用 handle.resume()
  │
  ▼
协程在线程 A 中恢复
  │
  ├── await_resume()
  ├── 输出第一次等待结束
  ├── co_await SleepFor{1000ms}
  │
  ▼
创建工作线程 B
  │
  ▼
协程再次暂停
  │
  ▼
工作线程 A 结束

工作线程 B:

text 复制代码
工作线程 B
  │
  ├── sleep_for(1 秒)
  │
  ▼
调用 handle.resume()
  │
  ▼
协程在线程 B 中恢复
  │
  ├── 输出第二次等待结束
  ├── finished.count_down()
  │
  ▼
协程结束并自动销毁协程帧

最后:

text 复制代码
finished.count_down()
        │
        ▼
main 的 finished.wait() 返回
        │
        ▼
main 结束

十八、为什么不能在 main 中固定睡眠四秒

错误示例:

cpp 复制代码
int main()
{
    start_async_task();

    std::this_thread::sleep_for(
        std::chrono::seconds(4)
    );
}

这相当于假设:

text 复制代码
异步任务一定能在四秒内完成

但真实程序中可能发生:

text 复制代码
线程调度延迟
网络响应变慢
磁盘读取变慢
异步操作失败
系统负载过高
任务被取消

更严重的问题是:

text 复制代码
main 睡眠结束
    │
    ▼
对象被析构
    │
    ▼
协程帧被销毁
    │
    ▼
异步线程稍后调用旧 handle.resume()
    │
    ▼
访问已经释放的协程帧
    │
    ▼
未定义行为

所以应该使用明确的完成通知:

text 复制代码
std::latch
std::condition_variable
std::future
任务调度器
事件循环
完成回调

十九、使用 co_yield 实现生成器

生成器是一种特殊协程:

text 复制代码
每次产生一个值
然后暂停
下一次请求时继续产生下一个值

例如:

cpp 复制代码
co_yield 3;
co_yield 4;
co_yield 5;

执行过程:

text 复制代码
调用 next()
  │
  ▼
执行到 co_yield 3
  │
  ├── 保存 3
  └── 暂停
  │
  ▼
调用者读取 3
  │
  ▼
再次调用 next()
  │
  ▼
从上次位置继续
  │
  ▼
执行到 co_yield 4

19.1 完整生成器代码

cpp 复制代码
#include <coroutine>
#include <exception>
#include <iostream>
#include <utility>

class IntGenerator
{
public:
    struct promise_type;

    using handle_type =
        std::coroutine_handle<promise_type>;

public:
    explicit IntGenerator(
        handle_type handle
    ) noexcept
        : handle_(handle)
    {
    }

    ~IntGenerator()
    {
        if (handle_)
        {
            handle_.destroy();
        }
    }

    IntGenerator(const IntGenerator&) = delete;

    IntGenerator& operator=(
        const IntGenerator&
    ) = delete;

    IntGenerator(
        IntGenerator&& other
    ) noexcept
        : handle_(
            std::exchange(other.handle_, {})
        )
    {
    }

    // 让生成器继续运行到下一个 co_yield。
    bool next()
    {
        if (!handle_ || handle_.done())
        {
            return false;
        }

        handle_.resume();

        // 如果协程内部发生异常,
        // 在调用 next() 的位置重新抛出。
        if (handle_.promise().exception_)
        {
            std::rethrow_exception(
                handle_.promise().exception_
            );
        }

        // 如果 resume 后协程还没有结束,
        // 表示它暂停在某个 co_yield。
        return !handle_.done();
    }

    // 获取最近一次 co_yield 产生的值。
    int value() const
    {
        return handle_.promise().current_value_;
    }

public:
    struct promise_type
    {
        // 保存最近一次 co_yield 的值。
        int current_value_ = 0;

        // 保存协程异常。
        std::exception_ptr exception_;

        IntGenerator get_return_object() noexcept
        {
            return IntGenerator{
                handle_type::from_promise(*this)
            };
        }

        // 生成器通常采用惰性启动。
        //
        // 调用 range() 时不会立即执行函数体,
        // 第一次调用 next() 时才开始执行。
        std::suspend_always
        initial_suspend() const noexcept
        {
            return {};
        }

        // 协程结束后保留协程帧,
        // 最后由 IntGenerator 析构函数销毁。
        std::suspend_always
        final_suspend() const noexcept
        {
            return {};
        }

        // co_yield value 最终会调用 yield_value(value)。
        std::suspend_always
        yield_value(int value) noexcept
        {
            // 保存当前产生的值。
            current_value_ = value;

            // 返回 suspend_always,
            // 表示产生一个值后立即暂停生成器。
            return {};
        }

        void return_void() const noexcept
        {
        }

        void unhandled_exception() noexcept
        {
            exception_ = std::current_exception();
        }
    };

private:
    handle_type handle_{};
};

// 生成 [begin, end) 范围内的整数。
IntGenerator range(int begin, int end)
{
    for (int value = begin;
         value < end;
         ++value)
    {
        // 保存当前值,并暂停生成器。
        co_yield value;
    }
}

int main()
{
    // 此时只创建协程帧。
    //
    // 因为 initial_suspend 返回 suspend_always,
    // 所以 range() 函数体尚未真正开始执行。
    IntGenerator numbers = range(3, 7);

    // 每调用一次 next(),
    // 生成器就继续运行到下一个 co_yield。
    while (numbers.next())
    {
        std::cout
            << "当前值:"
            << numbers.value()
            << '\n';
    }
}

输出:

text 复制代码
当前值:3
当前值:4
当前值:5
当前值:6

19.2 co_yield 的转换过程

代码:

cpp 复制代码
co_yield value;

可以大致理解为:

cpp 复制代码
co_await promise.yield_value(value);

我们的 yield_value()

cpp 复制代码
std::suspend_always
yield_value(int value) noexcept
{
    current_value_ = value;

    return {};
}

完整流程:

text 复制代码
执行 co_yield 3
      │
      ▼
调用 promise.yield_value(3)
      │
      ├── current_value_ = 3
      │
      └── 返回 suspend_always
      │
      ▼
生成器暂停
      │
      ▼
调用者读取 current_value_
      │
      ▼
再次调用 next()
      │
      ▼
handle.resume()
      │
      ▼
从上次 co_yield 后继续

二十、协程中的局部变量与生命周期

20.1 普通值通常可以跨越挂起点

cpp 复制代码
Task test()
{
    int value = 100;

    co_await std::suspend_always{};

    std::cout << value << '\n';
}

大致内存结构:

text 复制代码
协程帧
┌───────────────────────┐
│ promise_type          │
├───────────────────────┤
│ value = 100           │
├───────────────────────┤
│ 当前执行位置          │
└───────────────────────┘

所以恢复后 value 仍然有效。


20.2 引用不会自动延长对象生命周期

危险代码:

cpp 复制代码
Task print_later(const std::string& text)
{
    co_await std::suspend_always{};

    std::cout << text << '\n';
}

调用:

cpp 复制代码
Task task;

{
    std::string message = "hello";

    task = print_later(message);
}

// message 已经销毁。

task.resume();

可能形成:

text 复制代码
协程帧中的 text
      │
      │ 引用
      ▼
外部 message 对象
      │
      ▼
message 已经销毁
      │
      ▼
text 成为悬空引用

正确思路之一是按值传递:

cpp 复制代码
Task print_later(std::string text)
{
    co_await std::suspend_always{};

    std::cout << text << '\n';
}

这样字符串对象可以被保存到协程帧中。


20.3 this 指针也可能悬空

危险代码:

cpp 复制代码
class Worker
{
public:
    Task run()
    {
        co_await std::suspend_always{};

        // this 指向的 Worker 可能已经销毁。
        std::cout << value_ << '\n';
    }

private:
    int value_ = 100;
};

调用:

cpp 复制代码
Task task;

{
    Worker worker;

    task = worker.run();
}

// worker 已经析构。

task.resume();

协程帧中保存的只是:

text 复制代码
this 指针

并不会自动延长 Worker 对象的生命周期。


二十一、协程 Lambda 为什么容易出现问题

例如:

cpp 复制代码
int value = 100;

auto lambda = [&value]() -> Task
{
    co_await std::suspend_always{};

    std::cout << value << '\n';
};

需要同时考虑两个对象:

text 复制代码
Lambda 闭包对象
外部 value 对象

生命周期关系:

text 复制代码
协程帧
  │
  ├── 可能保存闭包对象的 this 指针
  │
  └── 闭包对象内部保存 value 的引用
                    │
                    ▼
             外部 value 对象

只要其中一个提前销毁,恢复协程时就可能发生悬空访问。

对于入门阶段,建议:

text 复制代码
协程 Lambda 尽量避免引用捕获
需要跨越挂起点的数据尽量按值传递
明确保证对象生命周期长于协程

二十二、协程常见使用场景

22.1 网络服务器

cpp 复制代码
Task handle_client(Socket socket)
{
    while (true)
    {
        std::string request =
            co_await socket.async_read();

        std::string response =
            process_request(request);

        co_await socket.async_write(response);
    }
}

执行模型:

text 复制代码
等待客户端数据
    │
    ▼
协程暂停
    │
    ▼
事件循环处理其他连接
    │
    ▼
客户端数据到达
    │
    ▼
恢复对应协程

22.2 定时任务

cpp 复制代码
Task update_loop()
{
    while (true)
    {
        update();

        co_await sleep_for(100ms);
    }
}

22.3 游戏脚本

cpp 复制代码
Task character_action()
{
    play_animation("attack");

    co_await wait_animation_finished();

    apply_damage();

    co_await sleep_for(500ms);

    return_to_idle();
}

22.4 惰性序列

cpp 复制代码
Generator<int> fibonacci()
{
    int a = 0;
    int b = 1;

    while (true)
    {
        co_yield a;

        int next = a + b;
        a = b;
        b = next;
    }
}

二十三、协程不能自动解决什么问题

23.1 协程不会自动并行

cpp 复制代码
Task heavy_work()
{
    calculate_for_ten_seconds();

    co_return;
}

如果 calculate_for_ten_seconds() 在主线程执行,那么主线程仍然会被阻塞十秒。

协程不是:

text 复制代码
线程池
多线程
多进程
SIMD
GPU 并行

23.2 协程不会把阻塞 I/O 自动变成异步 I/O

cpp 复制代码
Task read_file()
{
    std::ifstream file("data.txt");

    file.read(...);

    co_return;
}

如果 file.read() 是阻塞调用,那么当前线程仍然会阻塞。

真正异步需要底层机制支持,例如:

text 复制代码
Linux io_uring
Windows IOCP
异步 Socket
epoll + 非阻塞 I/O
线程池包装阻塞操作

二十四、完整项目结构

text 复制代码
cpp20_coroutine_tutorial/
├── CMakeLists.txt
├── include/
│   └── task.hpp
├── 01_basic.cpp
├── 02_debug_awaiter.cpp
├── 03_manual_event.cpp
├── 04_async_timer.cpp
└── 05_generator.cpp

二十五、CMakeLists.txt

cmake 复制代码
cmake_minimum_required(VERSION 3.20)

project(
    cpp20_coroutine_tutorial
    LANGUAGES CXX
)

# 使用 C++20。
set(CMAKE_CXX_STANDARD 20)

# 必须支持 C++20,不能自动降级。
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 禁用 GNU 扩展,使用标准 C++。
set(CMAKE_CXX_EXTENSIONS OFF)

# 查找线程库。
find_package(Threads REQUIRED)

add_executable(
    01_basic
    01_basic.cpp
)

target_include_directories(
    01_basic
    PRIVATE include
)

add_executable(
    02_debug_awaiter
    02_debug_awaiter.cpp
)

target_include_directories(
    02_debug_awaiter
    PRIVATE include
)

add_executable(
    03_manual_event
    03_manual_event.cpp
)

target_include_directories(
    03_manual_event
    PRIVATE include
)

add_executable(
    04_async_timer
    04_async_timer.cpp
)

target_link_libraries(
    04_async_timer
    PRIVATE Threads::Threads
)

add_executable(
    05_generator
    05_generator.cpp
)

构建:

bash 复制代码
cmake -S . -B build \
    -DCMAKE_BUILD_TYPE=Debug

cmake --build build

二十六、学习协程时应该按什么顺序

不要一开始就去写完整异步框架。

推荐顺序如下。

第一阶段:理解暂停和恢复

只学习:

text 复制代码
co_await suspend_always
coroutine_handle.resume()
协程帧
Task 生命周期

对应示例:

text 复制代码
01_basic.cpp

第二阶段:理解 Awaiter

掌握:

cpp 复制代码
await_ready()
await_suspend()
await_resume()

对应示例:

text 复制代码
02_debug_awaiter.cpp

第三阶段:理解外部事件恢复协程

掌握:

text 复制代码
Awaiter 保存协程句柄
外部事件调用 handle.resume()

对应示例:

text 复制代码
03_manual_event.cpp

第四阶段:理解线程与协程的关系

掌握:

text 复制代码
协程在哪个线程恢复
取决于哪个线程调用 resume()

对应示例:

text 复制代码
04_async_timer.cpp

第五阶段:理解生成器

掌握:

text 复制代码
co_yield
yield_value()
惰性执行

对应示例:

text 复制代码
05_generator.cpp

二十七、最后建立完整思维模型

看到下面这行代码:

cpp 复制代码
Result result = co_await operation;

你应该在脑中展开成:

text 复制代码
1. 获取 operation 对应的 Awaiter
        │
        ▼
2. 调用 await_ready()
        │
        ├── true:操作已经完成
        │          │
        │          └──────────────┐
        │                         │
        └── false                 │
              │                   │
              ▼                   │
3. 调用 await_suspend(handle)     │
              │                   │
              ▼                   │
4. 保存或转交当前协程句柄          │
              │                   │
              ▼                   │
5. 当前协程暂停                    │
              │                   │
              ▼                   │
6. 调用线程继续执行其他工作         │
              │                   │
              ▼                   │
7. 外部异步操作完成                 │
              │                   │
              ▼                   │
8. 外部系统调用 handle.resume()     │
              │                   │
              └───────────────────┤
                                  ▼
9. 调用 await_resume()
        │
        ▼
10. 获取结果或抛出异常
        │
        ▼
11. result 得到返回值
        │
        ▼
12. 协程继续执行后续代码

同时记住下面六句话:

text 复制代码
1. 协程不是线程。

2. 协程不会自动创建线程。

3. 协程不会自动把阻塞函数变成异步函数。

4. 协程帧负责保存局部变量、执行位置和 promise_type。

5. coroutine_handle 负责恢复和销毁协程。

6. Awaiter 决定协程是否暂停,以及将来如何恢复。

最终可以把 C++20 协程归纳为:

text 复制代码
                  C++20 协程
                       │
        ┌──────────────┼──────────────┐
        │              │              │
        ▼              ▼              ▼
     协程帧          promise_type     Awaiter
        │              │              │
        │              │              ├── await_ready
        │              │              ├── await_suspend
        │              │              └── await_resume
        │              │
        │              ├── initial_suspend
        │              ├── final_suspend
        │              ├── return_void/value
        │              └── unhandled_exception
        │
        ├── 保存局部变量
        ├── 保存函数参数
        ├── 保存执行位置
        └── 保存等待状态

协程真正的核心并不是记住所有模板代码,而是理解:

text 复制代码
函数运行到某个位置
        │
        ▼
保存当前状态
        │
        ▼
暂停并交还控制权
        │
        ▼
外部操作完成
        │
        ▼
通过句柄恢复函数
        │
        ▼
从原来的位置继续执行

后续教程应继续扩展 Task<T>、协程之间互相 co_awaitcontinuation、对称转移,以及基于 epoll 的真实异步 Socket 示例。

相关推荐
无限的鲜花11 天前
协程本质是函数加状态机——零基础深入浅出 C++20 协程
c++·算法·c++20
十五年专注C++开发1 个月前
全面深入了解C++20 范围库(std::ranges)
c++20·管道·哨兵·视图·ranges
小小龙学IT1 个月前
C++20 协程深度解析:从原理到高性能异步框架实战
junit·c++20
楼田莉子1 个月前
C++20新特性:协程
开发语言·c++·后端·学习·c++20
ouliten1 个月前
C++笔记:C++20风格线程池
c++·笔记·c++20
眠りたいです1 个月前
现代C++:C++17中的新库特性
开发语言·c++·c++20·c++17
楼田莉子2 个月前
C++20新特性:Range库
开发语言·c++·后端·学习·c++20
楼田莉子2 个月前
C++20现代特性:概念与约束
开发语言·c++·后端·学习·c++20
aluluka2 个月前
C++ 20 协程的探索
c++·c++20