**C++20协程:从基础到实战**

在 C++20 中,协程(coroutines)被正式引入,彻底改变了异步编程的方式。它们让我们可以用顺序代码来描述异步流程,从而大幅提升代码的可读性与可维护性。本文将从协程的概念、核心语法、实现原理,到一个完整的实战案例,逐步带你走进协程的世界。


1. 协程到底是什么?

协程是一种能够暂停与恢复执行的函数(或生成器)。与普通函数不同,协程可以在执行过程中挂起自己,随后再恢复继续执行。它们是 轻量级 的线程化工具,内部不需要调度器、线程栈等复杂机制。

在 C++20 中,协程通过四个关键关键词实现:

关键词 作用
co_await 等待一个异步操作完成
co_yield 产生一个值(生成器)
co_return 返回协程结果
co_suspend 手动挂起协程

协程的返回类型不是普通类型,而是 std::coroutine_handle 或者更常见的 std::futurestd::generator 等适配器。


2. 基本语法

2.1 定义协程

#include <coroutine>
#include <iostream>
#include <string>

struct Awaitable {
    std::string value;
    Awaitable(std::string v) : value(std::move(v)) {}
    bool await_ready() const noexcept { return false; }   // 需要挂起
    void await_suspend(std::coroutine_handle<> h) const noexcept {
        std::cout << "Suspending: " << value << '\n';
    }
    std::string await_resume() const noexcept { return value; }
};

std::string asyncHello() {
    std::string result = co_await Awaitable("Hello, coroutine!");
    return result + " World!";
}
  • await_ready:判断是否需要挂起。若返回 true,协程直接继续执行,否则挂起。
  • await_suspend:挂起时的回调。通常会把协程句柄传给外部事件循环或线程池。
  • await_resume:挂起结束后,返回值给调用者。

2.2 调用协程

int main() {
    std::string res = asyncHello(); // 这行不会直接得到结果
    // 实际上会在 await_suspend 里挂起,之后手动恢复
}

但在真实应用中,协程通常与 事件循环异步运行时 配合使用。


3. 协程的实现原理

协程在编译器层面会被展开为一个 状态机。每一次 co_awaitco_yieldco_return 对应状态机的一个分支。编译器会生成:

  1. Promise:承载协程的状态、返回值、异常。
  2. Suspend/Resume:实现挂起/恢复逻辑。
  3. Handle:用于外部控制协程生命周期。

重要点:协程本身不需要堆栈,它们使用 单一栈(通常是调用者栈)来保存局部变量;而状态机的状态则由编译器维护在堆上。


4. 一个完整实战:异步文件读取

以下示例演示如何使用协程实现一个简单的异步文件读取,并与 std::async 结合完成 I/O 线程池。

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

struct AsyncRead {
    std::ifstream &ifs;
    std::string buffer;
    size_t size;

    AsyncRead(std::ifstream &f, size_t sz) : ifs(f), size(sz) {}

    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> h) const noexcept {
        std::thread([h, this](){
            buffer.resize(size);
            ifs.read(buffer.data(), size);
            std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟异步延迟
            h.resume();
        }).detach();
    }
    std::string await_resume() const noexcept { return buffer; }
};

std::string asyncReadFile(std::ifstream &file, size_t sz) {
    std::string data = co_await AsyncRead(file, sz);
    return data;
}

int main() {
    std::ifstream file("sample.txt", std::ios::binary);
    if (!file) { std::cerr << "open failed\n"; return 1; }

    std::string result = asyncReadFile(file, 1024);
    std::cout << "Read data: " << result.substr(0, 100) << "...\n";
}

说明:

  • AsyncReadawait_suspend 中创建了一个线程来执行真正的 I/O 操作,完成后通过 h.resume() 恢复协程。
  • 主程序等待协程完成后继续执行。

在生产环境中,你会用更完善的线程池、任务调度器来管理协程的挂起与恢复。


5. 协程 vs. 线程

特性 协程 线程
内存占用 轻量(几 KB) 大量(几 MB)
上下文切换 只在用户态 需要操作系统切换
并发数 数千到数百万 受限于系统资源
编程模型 线性、可读性强 需要锁、消息传递
适用场景 I/O 密集、网络服务、游戏循环 CPU 密集、硬件控制

6. 小结

  • C++20 的协程提供了更自然、更可读的异步编程方式。
  • 关键关键词 co_awaitco_yieldco_return 控制协程的挂起与恢复。
  • 协程通过状态机实现,内部无需完整栈,降低资源消耗。
  • 与事件循环、线程池结合,可构建高性能网络框架、游戏引擎、数据流处理。

进一步学习建议:阅读《C++协程实战》或使用 Boost.Coroutine2cppcoro 等库深入理解。

祝你在协程的世界里玩得开心,写出高效优雅的 C++ 代码!

发表评论