C++20 之 Coroutines 协程

在 C++20 之前,想要写异步代码,你要么忍受回调地狱,要么手写复杂的状态机,要么依赖 Boost.Coroutine 这样的第三方库。C++20 终于在语言层面引入了协程(Coroutines)——一种可以暂停和恢复的函数。有了它,异步代码可以写成同步的样子,生成器可以像 Python 的 yield 一样优雅,任务调度也有了更清晰的抽象。


一、为什么需要协程?

先看看传统异步编程的痛点。

痛点一:回调地狱

异步 I/O 操作(网络请求、文件读写)通常依赖回调:

void fetchUserData(const std::string& userId) {
    asyncQuery("SELECT * FROM users WHERE id=" + userId,
        [](std::string userData) {
            asyncParse(userData, [](User user) {
                asyncSaveCache(user, [](bool success) {
                    asyncNotify(user.email, "Profile updated", []() {
                        std::cout << "All done!" << std::endl;
                    });
                });
            });
        });
}

嵌套四层回调,逻辑支离破碎,错误处理无从下手。这就是所谓的 “回调地狱”(Callback Hell)

痛点二:手写状态机

为了解决回调嵌套,开发者往往把异步操作拆解成状态机:

enum class State { Idle, QuerySent, DataReceived, Parsed, Saved };

struct FetchTask {
    State state = State::Idle;
    std::string userId;
    User user;

    void resume() {
        switch (state) {
            case State::Idle:
                sendQuery(userId);
                state = State::QuerySent;
                break;
            case State::QuerySent:
                user = parse(data);
                state = State::DataReceived;
                break;
            // ... 还有更多状态
        }
    }
};

状态机虽然解决了嵌套问题,但代码量暴增、逻辑散落在各个状态分支中,可读性和可维护性都很差。

痛点三:第三方库不统一

Boost.Coroutine、libco、ucontext……各家用法不一,移植成本高。C++ 需要一个语言级别的标准方案

C++20 的协程,就是这个标准方案。


二、什么是协程?

2.1 用户态线程 vs 协程

很多人第一次听到协程会联想到线程。它们的对比:

对比项 线程(Thread) 协程(Coroutine)
调度方式 操作系统内核调度 用户态调度(手动/框架)
切换开销 高(陷入内核态) 极低(仅保存/恢复寄存器)
并发数量 通常数千级别 可达百万级别
同步原语 需要锁、信号量 可用无锁设计
资源消耗 每个线程 1MB+ 栈空间 仅需几 KB

一句话总结:协程是用户态的可暂停函数。它在线程内部运行,由程序员或调度框架控制何时暂停、何时恢复,不需要操作系统的介入。

2.2 协程的本质

协程的核心能力只有两个:

  1. 暂停(suspend):函数执行到一半可以暂停,把当前状态保存起来
  2. 恢复(resume):之后可以从暂停点恢复执行,就像从未离开过一样
协程函数 调用者 协程函数 调用者 做其他事情... 调用 执行到 co_await 暂停,返回控制权 恢复执行 从暂停点继续 协程完成

三、核心关键字

C++20 引入了三个新关键字来标识协程:

co_await — 暂停并等待

auto result = co_await asyncOperation();

协程执行到 co_await 时暂停,等待操作完成后恢复。如果操作已经完成,可以立即恢复;如果没完成,协程会挂起,把控制权交还给调用者。

co_yield — 产出值并暂停

co_yield value;  // 等价于 co_await promise.yield_value(value)

常用于生成器场景。协程产出一个值后暂停,等调用者请求下一个值时再恢复。

co_return — 协程返回

co_return result;  // 协程结束,返回结果

协程的 “return” 语句。执行到 co_return 时协程结束(销毁),可以通过 promise_type::return_value()return_void() 传递返回值。

重要提示: 只要函数体中出现了 co_awaitco_yieldco_return 中的任意一个,编译器就会将该函数识别为协程。协程的返回类型不能是 void 以外的普通类型——必须配合协程框架(自定义类型或标准库的 std::coroutine_handle)使用。


四、协程的三大组件

C++20 的协程是一个"零开销抽象框架"——标准库只定义了机制,不提供默认实现。你需要自定义(或使用第三方库提供的)三个核心组件:

持有

co_await 时使用

CoroutineHandle

+resume()

+destroy()

+promise() : -> Promise&

Promise

+get_return_object()

+initial_suspend()

+final_suspend()

+return_value(T)

+yield_value(T)

+unhandled_exception()

Awaitable

+await_ready() : -> bool

+await_suspend(handle)

+await_resume() : -> T

«实现 Awaitable»

Awaiter

4.1 Promise 类型(承诺类型)

Promise 是协程的"管家",负责:

  • 创建返回对象get_return_object() 决定协程返回什么
  • 控制初始挂起点initial_suspend() 返回 suspend_always(懒启动)或 suspend_never(立即执行)
  • 控制最终挂起点final_suspend() 返回的 Awaitable 决定协程结束时是否挂起
  • 处理返回值return_value(T)return_void()
  • 处理 co_yieldyield_value(T)
  • 处理异常unhandled_exception() 捕获协程内部未处理的异常

4.2 Awaitable/Awaiter(可等待对象)

当你写 co_await someObj 时,someObj 必须是一个 Awaitable,即提供三个方法:

方法 作用
await_ready() 如果返回 true,不需要挂起,直接获取结果
await_suspend(handle) 挂起时执行,可以将协程句柄保存起来以便之后恢复
await_resume() 恢复时调用,返回操作的结果

一个简单的 awaiter 示例——立即就绪的 awaiter:

struct ReadyAwaiter {
    bool await_ready() { return true; }                    // 不需要挂起
    void await_suspend(std::coroutine_handle<>) {}         // 无操作
    int await_resume() { return 42; }                      // 直接返回 42
};

4.3 Coroutine Handle(协程句柄)

std::coroutine_handle<> 是协程的"遥控器",可以用来:

  • resume():恢复协程执行
  • destroy():销毁协程对象并释放栈帧
  • done():检查协程是否已完成
auto handle = ...;  // 获取协程句柄
if (!handle.done()) {
    handle.resume();  // 恢复执行
}
handle.destroy();     // 销毁协程

⚠️ 注意: Handle 本身不拥有协程的生命周期。你必须在适当的时候手动 destroy(),否则会导致内存泄漏。


五、实战示例

示例 1:简单生成器(Generator)

生成器是最直观的协程用法——按需产出一系列值。

#include <iostream>
#include <coroutine>
#include <optional>

// 简单的 Generator 实现
template<typename T>
class Generator {
public:
    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 = std::move(value);
            return {};
        }

        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    // 迭代器支持
    struct iterator {
        std::coroutine_handle<promise_type> handle;

        bool operator!=(std::default_sentinel_t) const {
            return !handle.done();
        }

        const T& operator*() const {
            return handle.promise().current_value;
        }

        iterator& operator++() {
            handle.resume();
            return *this;
        }
    };

    explicit Generator(std::coroutine_handle<promise_type> h) : handle_(h) {}
    ~Generator() { if (handle_) handle_.destroy(); }

    // 禁止拷贝,允许移动
    Generator(const Generator&) = delete;
    Generator& operator=(const Generator&) = delete;
    Generator(Generator&& other) noexcept : handle_(std::exchange(other.handle_, {})) {}

    iterator begin() {
        handle_.resume();  // 启动到第一个 yield
        return {handle_};
    }

    std::default_sentinel_t end() { return {}; }

private:
    std::coroutine_handle<promise_type> handle_;
};

// 使用生成器
Generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        auto temp = a;
        a = b;
        b = temp + b;
    }
}

int main() {
    // 生成前 10 个斐波那契数
    int count = 0;
    for (auto num : fibonacci()) {
        std::cout << num << " ";
        if (++count >= 10) break;
    }
    std::cout << std::endl;
    // 输出: 0 1 1 2 3 5 8 13 21 34
}

关键点:

  • initial_suspend() 返回 suspend_always 实现懒启动——协程不会立即执行,等 begin() 被调用才启动
  • co_yield 将值保存到 promise_type::current_value,然后挂起
  • final_suspend() 也返回 suspend_always,协程结束后句柄仍然有效,可以检查 done()
  • 通过 begin()/end() 实现 range-based for 循环支持

示例 2:异步任务(Task)

这是最常见的协程应用——把异步操作包装成可以 co_await 的 Task。

#include <iostream>
#include <coroutine>
#include <string>
#include <future>
#include <thread>
#include <chrono>

// 简化的 Task 类型
template<typename T = void>
class Task {
public:
    struct promise_type {
        T result;

        Task get_return_object() {
            return Task{
                std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }

        std::suspend_always initial_suspend() { return {}; }  // 懒启动
        std::suspend_always final_suspend() noexcept { return {}; } // 保持帧存活

        void return_value(T value) { result = std::move(value); }
        void unhandled_exception() { std::terminate(); }
    };

    explicit Task(std::coroutine_handle<promise_type> h) : handle_(h) {}
    ~Task() { if (handle_) handle_.destroy(); }

    Task(const Task&) = delete;
    Task(Task&& other) noexcept : handle_(std::exchange(other.handle_, {})) {}

    // 实现 Awaitable 接口,支持 co_await
    auto operator co_await() const noexcept {
        struct Awaiter {
            std::coroutine_handle<promise_type> handle;
            bool await_ready() const noexcept { return handle.done(); }
            void await_suspend(std::coroutine_handle<> awaiting) noexcept {
                handle.resume();  // 简化版:同步恢复
            }
            T await_resume() { return std::move(handle.promise().result); }
        };
        return Awaiter{handle_};
    }

    void resume() { handle_.resume(); }

    T get_result() {
        return std::move(handle_.promise().result);
    }

private:
    std::coroutine_handle<promise_type> handle_;
};

// 模拟异步操作
Task<std::string> fetchUserData(const std::string& userId) {
    // 模拟网络延迟
    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    co_return "User{" + userId + ", name: \"LinXi\"}";
}

Task<std::string> fetchUserPosts(const std::string& userId) {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    co_return "Posts for " + userId + ": [\"C++20 协程\", \"Ranges\"]";
}

// 协程链式调用:获取用户信息后获取帖子
Task<std::string> buildUserProfile(const std::string& userId) {
    std::cout << "[buildUserProfile] 开始获取用户数据..." << std::endl;

    auto userData = co_await fetchUserData(userId);
    std::cout << "[buildUserProfile] 用户数据: " << userData << std::endl;

    auto posts = co_await fetchUserPosts(userId);
    std::cout << "[buildUserProfile] 用户帖子: " << posts << std::endl;

    co_return userData + " | " + posts;
}

int main() {
    auto task = buildUserProfile("u001");
    task.resume();  // 启动协程
    std::cout << "\n最终结果: " << task.get_result() << std::endl;
}

输出:

[buildUserProfile] 开始获取用户数据...
[buildUserProfile] 用户数据: User{u001, name: "LinXi"}
[buildUserProfile] 用户帖子: Posts for u001: ["C++20 协程", "Ranges"]

最终结果: User{u001, name: "LinXi"} | Posts for u001: ["C++20 协程", "Ranges"]

注意: 这里使用了简化的 Task 实现来展示核心概念。生产环境建议使用成熟的协程库(如 cppcoro、asyncio 等),它们提供了完整的 co_await 支持和真正的异步 I/O 集成。

示例 3:co_await 的工作原理

这个示例深入展示 co_await 的三个阶段:

#include <iostream>
#include <coroutine>
#include <chrono>

// 自定义 Awaiter:延迟指定毫秒后就绪
class DelayAwaiter {
public:
    explicit DelayAwaiter(int ms) : milliseconds_(ms) {}

    bool await_ready() {
        return false;  // 总是需要挂起
    }

    void await_suspend(std::coroutine_handle<> handle) {
        std::cout << "  [await_suspend] 挂起协程,启动定时器 " << milliseconds_ << "ms" << std::endl;

        // 在实际应用中,这里会注册定时器回调
        // 定时器到期后调用 handle.resume()
        // 这里用 sleep 模拟
        std::thread([this, handle]() {
            std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds_));
            std::cout << "  [定时器] " << milliseconds_ << "ms 到期,恢复协程" << std::endl;
            handle.resume();  // 恢复协程
        }).detach();
    }

    void await_resume() {
        std::cout << "  [await_resume] 协程已恢复,继续执行" << std::endl;
    }

private:
    int milliseconds_;
};

// 简单协程
struct SimpleTask {
    struct promise_type {
        SimpleTask get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

SimpleTask simpleCoroutine() {
    std::cout << "[协程] 开始执行" << std::endl;

    co_await DelayAwaiter(200);  // 暂停 200ms

    std::cout << "[协程] 恢复后继续执行" << std::endl;

    co_await DelayAwaiter(100);  // 再暂停 100ms

    std::cout << "[协程] 全部完成!" << std::endl;
}

int main() {
    std::cout << "=== co_await 工作流程演示 ===" << std::endl;

    auto coro = simpleCoroutine();

    std::cout << "[主线程] 协程已启动,主线程继续工作" << std::endl;

    // 等待协程完成(实际应用中会有事件循环)
    std::this_thread::sleep_for(std::chrono::seconds(1));

    std::cout << "[主线程] 程序结束" << std::endl;
}

执行流程:

定时器 协程函数 主线程 定时器 协程函数 主线程 暂停 ⏸️ 恢复 ▶️ 再次暂停 ⏸️ 恢复 ▶️ 启动协程 "开始执行" co_await DelayAwaiter(200) "协程已启动,主线程继续" 200ms 后 resume() "恢复后继续执行" co_await DelayAwaiter(100) 100ms 后 resume() "全部完成!"

示例 4:异常处理

协程中的异常会被 Promise 捕获,需要妥善处理:

#include <iostream>
#include <coroutine>
#include <stdexcept>

// 带异常处理的 Task
class TaskEx {
public:
    struct promise_type {
        bool has_exception = false;
        std::exception_ptr exception;

        TaskEx get_return_object() {
            return TaskEx{
                std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }

        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        void return_void() {}

        void unhandled_exception() {
            exception = std::current_exception();
            has_exception = true;
        }
    };

    explicit TaskEx(std::coroutine_handle<promise_type> h) : handle_(h) {}
    ~TaskEx() { if (handle_) handle_.destroy(); }
    TaskEx(TaskEx&& other) noexcept : handle_(std::exchange(other.handle_, {})) {}
    TaskEx(const TaskEx&) = delete;

    void resume() { handle_.resume(); }

    void get_result() {
        if (handle_.promise().has_exception) {
            std::rethrow_exception(handle_.promise().exception);
        }
    }

private:
    std::coroutine_handle<promise_type> handle_;
};

TaskEx riskyOperation() {
    std::cout << "执行可能失败的操作..." << std::endl;

    bool success = false;  // 模拟失败

    if (!success) {
        throw std::runtime_error("操作失败:网络连接超时");
    }

    co_return;
}

int main() {
    auto task = riskyOperation();
    task.resume();

    try {
        task.get_result();
    } catch (const std::exception& e) {
        std::cerr << "捕获协程异常: " << e.what() << std::endl;
    }
}

六、注意事项

1. 协程的生命周期管理

这是使用协程最容易踩坑的地方:

// ❌ 危险:协程引用了局部变量,协程挂起后变量已销毁
// ❌ 另外,auto 不能用作协程返回类型
SimpleTask badCoroutine() {
    int localVar = 42;
    co_await someOperation();
    // localVar 可能已经不存在了!
}

// ✅ 安全:通过值捕获或使用 shared_ptr,且使用明确的返回类型
Task<int> goodCoroutine(int value) {
    auto sharedData = std::make_shared<int>(value);
    co_await someOperation();
    // sharedData 仍然有效
    co_return *sharedData;
}

原则: 协程帧(coroutine frame)中的所有变量在 co_await 暂点之间都必须保持有效。不要捕获引用指向栈上的局部变量。

2. 异常处理

  • 协程中未捕获的异常会传播到 promise_type::unhandled_exception()
  • 如果你没有实现 unhandled_exception(),默认行为是 std::terminate()
  • 最佳实践: 始终实现 unhandled_exception(),至少记录日志
void unhandled_exception() {
    // 方式一:存储异常,稍后处理
    exception_ = std::current_exception();
    // 方式二:终止程序(仅在不可恢复的错误时使用)
    // std::terminate();
}

3. 性能考量

协程本身几乎零开销,但需要注意:

关注点 说明
协程帧分配 每次创建协程会在堆上分配一帧(约几十字节),高频创建时考虑对象池
挂起/恢复 仅保存/恢复少量寄存器,开销约几十纳秒
编译时间 协程的模板展开会增加编译时间
调试难度 协程的执行流不直观,断点调试较困难

4. 不要滥用协程

协程不是银弹。适合用协程的场景:

  • ✅ 异步 I/O(网络、文件)
  • ✅ 生成器/迭代器
  • ✅ 任务调度/协程池
  • ✅ 游戏中的状态机

不适合的场景:

  • ❌ 简单的同步计算(直接写普通函数就好)
  • ❌ CPU 密集型并行计算(用 std::thread 或 OpenMP)
  • ❌ 需要简单、直观的控制流时

七、编译器支持

C++20 协程需要较新版本的编译器:

编译器 最低版本 备注
GCC 10.0+ 基本支持;13+ 较完善
Clang 14.0+ 较完善;建议用最新稳定版
MSVC VS 2019 16.10+ 建议用 VS 2022 以获得最佳支持

编译标志:

# GCC
g++ -std=c++20 -fcoroutines main.cpp -o main

# Clang
clang++ -std=c++20 -stdlib=libc++ main.cpp -o main

# MSVC (VS 2022)
cl /std:c++20 main.cpp

注意: MSVC 默认启用协程支持,不需要额外标志。GCC 需要 -fcoroutines(GCC 10/11)或 -fcoroutines-ts。GCC 12+ 以 -std=c++20 即可。

推荐的第三方库

由于 C++20 标准只提供了协程的框架,没有提供开箱即用的 Task、Generator 等类型,推荐使用以下成熟库:

特点 链接
cppcoro 参考实现,功能全面 GitHub
asyncio 轻量级异步 I/O GitHub

总结

C++20 的协程是语言层面的重大革新,它解决了困扰 C++ 社区多年的异步编程难题:

  • 三个新关键字co_await(等待)、co_yield(产出)、co_return(返回)
  • 三大组件:Promise(承诺)、Awaitable(可等待对象)、Coroutine Handle(句柄)
  • 零运行时开销:协程的挂起/恢复仅需保存/恢复少量寄存器
  • 应用场景广泛:异步编程、生成器、任务调度
  • ⚠️ 学习曲线陡峭:理解 Promise 和 Awaitable 需要一些时间投入
  • ⚠️ 标准库支持有限:目前需要搭配第三方库使用

协程是 C++20 中最有深度的特性之一,一旦掌握,你会发现异步代码可以写得像同步代码一样优雅。


📌 下一篇预告: C++20 引入的另一个重量级特性——Ranges 库。它带来了函数式风格的 range 管道操作,彻底改变了数据处理的方式。views::filterviews::transformviews::take……敬请期待!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐