前置知识: C++

C++20协程

00:00
5 min Advanced 2026/6/14

C++20协程详解:co_await、co_yield、co_return。

概述

C++20 引入了原生协程支持,为异步编程和惰性求值提供了语言级别的基础设施。协程是一种可以暂停执行并在后续恢复的函数,通过 co_awaitco_yieldco_return 三个关键字实现函数的挂起与恢复。C++20 协程属于无栈协程(Stackless Coroutine),其状态保存在堆上分配的协程帧中,相比有栈协程开销更低。

协程的核心价值在于:让异步代码拥有同步代码的可读性,同时保持异步非阻塞的执行效率。在网络编程、IO 密集型应用和生成器场景中,协程能显著简化代码逻辑。

基础概念

协程关键字

  • co_await:挂起协程,等待异步操作完成并获取结果
  • co_yield:挂起协程并产出一个值,协程可在后续恢复继续执行
  • co_return:从协程返回,标志协程执行结束

协程的生命周期

一个协程函数被调用时,编译器会自动创建协程帧,其中包含以下核心组件:

  • promise_type:控制协程行为的策略对象,定义挂起、恢复和返回的行为
  • coroutine_handle:协程句柄,用于外部恢复或销毁协程
  • 协程帧:存储局部变量、参数和挂起点的状态

协程与普通函数的区别

普通函数一旦开始执行就会运行到结束,而协程可以在执行过程中暂停,将控制权交还给调用者,并在稍后从暂停的位置继续执行。函数体内只要出现了 co_awaitco_yieldco_return 中的任何一个,该函数就被编译器识别为协程。

快速上手

最简单的 Generator

#include <coroutine>
#include <iostream>

// 一个简化的 Generator 实现
template<typename T>
struct Generator {
    struct promise_type {
        T current_value;

        Generator get_return_object() {
            return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle<promise_type> handle;

    // 迭代器支持 range-based for
    struct iterator {
        std::coroutine_handle<promise_type> handle;
        bool operator!=(std::default_sentinel_t) const { return !handle.done(); }
        iterator& operator++() { handle.resume(); return *this; }
        T operator*() const { return handle.promise().current_value; }
    };
    iterator begin() { handle.resume(); return {handle}; }
    std::default_sentinel_t end() { return {}; }
};

// 使用 co_yield 生成整数序列
Generator<int> range(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;
    }
}

int main() {
    for (auto val : range(0, 5)) {
        std::cout << val << " ";  // 输出: 0 1 2 3 4
    }
    return 0;
}

详细用法

协程 Promise 类型

promise_type 是协程行为的核心控制器,每个协程都必须定义关联的 promise_type:

struct Task {
    struct promise_type {
        // 创建协程返回对象
        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        // 初始挂起策略:suspend_never 表示协程立即开始执行
        std::suspend_never initial_suspend() { return {}; }
        // 最终挂起策略:suspend_never 表示协程结束后自动销毁
        std::suspend_never final_suspend() noexcept { return {}; }
        // co_return 对应的处理
        void return_void() {}
        // 异常处理
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle<promise_type> handle;
};

co_await 机制详解

co_await 的执行流程分为三个步骤:

auto result = co_await awaitable;
// 步骤 1: 调用 awaitable.await_ready()
//   如果返回 true,表示结果已就绪,直接获取结果,不挂起协程
// 步骤 2: 如果 await_ready() 返回 false,调用 awaitable.await_suspend(handle)
//   handle 是当前协程的句柄,可在此安排恢复协程的时机
// 步骤 3: 协程恢复时,调用 awaitable.await_resume() 获取最终结果

自定义 Awaitable 对象:

#include <chrono>
#include <thread>

// 自定义延时等待对象
struct Delay {
    int milliseconds;

    // 检查是否已就绪(始终返回 false,需要等待)
    bool await_ready() const { return false; }

    // 挂起协程,在另一个线程上延时后恢复
    void await_suspend(std::coroutine_handle<> handle) const {
        std::thread([handle, ms = milliseconds]() {
            std::this_thread::sleep_for(std::chrono::milliseconds(ms));
            handle.resume();  // 延时结束后恢复协程
        }).detach();
    }

    // 恢复后获取结果(此处无返回值)
    void await_resume() const {}
};

// 使用自定义 Awaitable
Task delayedGreeting() {
    std::cout << "开始等待..." << std::endl;
    co_await Delay{1000};  // 等待 1000 毫秒
    std::cout << "等待结束!" << std::endl;
}

co_yield 与生成器

co_yield value 等价于 co_await promise.yield_value(value),用于惰性生成序列

// 斐波那契数列生成器
Generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        auto temp = a;
        a = b;
        b = temp + b;
    }
}

// 使用 take 视图获取前 10 个斐波那契数
int main() {
    int count = 0;
    for (auto val : fibonacci()) {
        std::cout << val << " ";
        if (++count >= 10) break;  // 输出: 0 1 1 2 3 5 8 13 21 34
    }
    return 0;
}

常见场景

异步任务链

// 模拟异步网络请求
struct AsyncRead {
    std::string data;
    bool await_ready() { return !data.empty(); }
    void await_suspend(std::coroutine_handle<> h) {
        // 模拟异步 IO 完成后恢复
        std::thread([this, h]() {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            data = "服务端响应数据";
            h.resume();
        }).detach();
    }
    std::string await_resume() { return data; }
};

Task fetchData() {
    std::cout << "发起请求..." << std::endl;
    auto response = co_await AsyncRead{};  // 异步等待
    std::cout << "收到: " << response << std::endl;
}

惰性数据管道

// 按行读取日志文件并过滤
Generator<std::string> readLines(const std::string& filename) {
    std::ifstream file(filename);
    std::string line;
    while (std::getline(file, line)) {
        co_yield line;
    }
}

// 使用时按需处理,不会一次性加载全部内容
for (auto& line : readLines("app.log")) {
    if (line.find("ERROR") != std::string::npos) {
        std::cout << line << std::endl;
    }
}

注意事项

  • C++20 协程语言特性,但标准库提供了基础框架(如 std::coroutine_handle),尚未提供现成的级抽象(如 std::generator 在 C++23 才正式加入),需要自实现或使用第三方
  • 协程帧在分配,频繁创建短生命周期协程可能带来内存开销,应考虑复用协程或使用自定义分配器
  • final_suspend 返回 suspend_never协程帧会自动销毁返回 suspend_always 时需要手动销毁,否则会内存泄漏
  • 协程抛出异常会触发 unhandled_exception(),默认实现通常调用 std::terminate()生产代码中应妥善处理
  • 协程调试较为困难,建议开发加入日志追踪挂起和恢复的时机
  • co_await 的 Awaitable 对象必须正确实现三个方法,否则编译错误信息可能晦涩难懂

进阶用法

symmetric transfer 避免栈溢出

递归协程场景中,await_suspend 返回 std::coroutine_handle 可以实现对称转移,避免无限递归导致的溢出:

struct RecursiveAwaiter {
    std::coroutine_handle<> next;

    bool await_ready() { return false; }

    // 返回 coroutine_handle 实现对称转移
    // 编译器保证在挂起当前协程后立即恢复目标协程,不增加调用栈深度
    std::coroutine_handle<> await_suspend(std::coroutine_handle<>) {
        return next;  // 直接转移到下一个协程
    }

    void await_resume() {}
};

协程与线程池结合

#include <queue>
#include <mutex>
#include <condition_variable>

// 简易线程池调度器
class ThreadPool {
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex mtx;
    std::condition_variable cv;
    bool stop = false;

public:
    ThreadPool(size_t threads) {
        for (size_t i = 0; i < threads; ++i) {
            workers.emplace_back([this]() {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock lock(mtx);
                        cv.wait(lock, [this]() { return stop || !tasks.empty(); });
                        if (stop && tasks.empty()) return;
                        task = std::move(tasks.front());
                        tasks.pop();
                    }
                    task();
                }
            });
        }
    }

    void submit(std::function<void()> task) {
        {
            std::unique_lock lock(mtx);
            tasks.push(std::move(task));
        }
        cv.notify_one();
    }
};

// 在线程池上恢复协程的 Awaitable
struct ThreadPoolAwaiter {
    ThreadPool& pool;
    bool await_ready() { return false; }
    void await_suspend(std::coroutine_handle<> handle) {
        pool.submit([handle]() { handle.resume(); });
    }
    void await_resume() {}
};

使用 C++23 std::generator

C++23 正式引入了 std::generator,无需手动实现 Generator 即可使用:

#include <generator>

// C++23 标准生成器,代码更简洁
std::generator<int> range23(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;
    }
}

// 支持引用类型,避免拷贝
std::generator<const std::string&> getNames(const std::vector<std::string>& names) {
    for (const auto& name : names) {
        co_yield name;  // 按引用产出,零拷贝
    }
}

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式