**C++20 协程的核心原理与实际应用**

协程是 C++20 对异步编程的一大补充,它们让我们能够以同步的方式编写异步逻辑,代码更直观、易维护。本文从协程的实现机制、关键概念到实际示例,系统梳理协程的核心原理与使用技巧。


1. 协程是什么?

协程(coroutine)是一种轻量级的用户态线程,允许在函数内部暂停(co_awaitco_yieldco_return)并在随后恢复执行。与传统的回调或 std::asyncstd::future 等相比,协程能够:

  • 保持状态:在挂起点之间保存局部变量。
  • 透明控制流:看似同步的写法,内部实现异步。
  • 高效切换:协程切换的成本远低于线程切换。

2. 关键概念

关键字 作用 典型用法
co_await 暂停协程,等待一个可等待对象(awaitable)。 int x = co_await some_async_task();
co_yield 暂停协程并返回一个值,后续再次进入时继续执行。 co_yield i;
co_return 结束协程,返回一个值。 co_return result;

Awaitable

一个对象若能被 co_await,就称为 awaitable。它至少要实现:

  • bool await_ready() noexcept; 立即完成?
  • void await_suspend(std::coroutine_handle<>) noexcept; 挂起时的操作。
  • T await_resume() noexcept; 恢复后返回的值。

C++20 标准库提供了一些基础 awaitable,如 std::future, std::generator 等。

Coroutine Handle

协程生成后返回 std::coroutine_handle<>,可用于手动恢复、检查状态、销毁协程。


3. 实现细节

协程在编译阶段被转换为 状态机

  1. 生成:编译器为每个 co_* 点生成一个分支。
  2. 挂起co_await 会把当前栈帧状态保存在 heap 或专用内存结构中。
  3. 恢复:再次调用 resume() 时,编译器会跳转到下一个分支,继续执行。

这就是协程为何能在暂停时保留局部变量的原因——它们不再依赖栈,而是通过堆或专门的 promise 对象维护。


4. 一个完整示例:异步读取文件行

#include <iostream>
#include <fstream>
#include <coroutine>
#include <string>
#include <vector>

// 简易 awaitable,异步读取一行
struct AsyncReadLine {
    std::ifstream& stream;
    std::string line;
    bool await_ready() noexcept { return false; } // 永远挂起
    void await_suspend(std::coroutine_handle<> h) noexcept {
        std::thread([this, h]() {
            if (std::getline(stream, line)) {
                // 读取成功,恢复协程
                h.resume();
            } else {
                // 读取失败(EOF),直接恢复
                h.resume();
            }
        }).detach();
    }
    std::string await_resume() noexcept { return std::move(line); }
};

struct LineReader {
    struct promise_type {
        std::string current;
        std::coroutine_handle <promise_type> next;
        std::vector<std::string> buffer;

        LineReader get_return_object() {
            return LineReader{ std::coroutine_handle <promise_type>::from_promise(*this) };
        }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }

        // 生成器:每次 co_yield 当前行
        std::suspend_always yield_value(std::string&& value) noexcept {
            buffer.push_back(std::move(value));
            return {};
        }
    };

    std::coroutine_handle <promise_type> handle;

    // 析构释放协程
    ~LineReader() { if (handle) handle.destroy(); }

    // 迭代器实现
    struct iterator {
        std::vector<std::string> buffer;
        size_t idx = 0;
        iterator() = default;
        bool operator!=(const iterator& other) const { return idx != other.idx; }
        const std::string& operator*() const { return buffer[idx]; }
        iterator& operator++() { ++idx; return *this; }
    };

    iterator begin() {
        handle.resume(); // 开始执行协程
        return iterator{ handle.promise().buffer, 0 };
    }
    iterator end() { return iterator{}; }
};

LineReader read_lines(std::ifstream& file) {
    while (true) {
        AsyncReadLine read{file};
        std::string line = co_await read;
        if (line.empty() && file.eof()) break;
        co_yield std::move(line);
    }
}

int main() {
    std::ifstream infile("example.txt");
    if (!infile) return 1;

    for (const auto& line : read_lines(infile)) {
        std::cout << line << '\n';
    }
}

说明

  • AsyncReadLine 是一个 awaitable,内部使用 std::thread 异步读取文件行。
  • read_lines 协程通过 co_await 挂起等待读取结果,然后 co_yield 返回每行。
  • LineReader 封装协程,提供 begin()/end() 迭代器,让 for-each 能直接使用。

5. 与 std::asyncstd::future 对比

维度 std::async C++20 协程
语义 线程池/线程封装 状态机,轻量级
锁/竞争 需要同步 通常不需要
错误处理 future::get 抛异常 通过 await_resume 抛异常
代码风格 回调式或 Future/Promise 直观同步式
性能 线程上下文切换 只需堆分配 + 函数指针跳转

6. 常见坑与最佳实践

  1. 避免在 await_suspend 中使用 std::async
    std::async 本身会创建线程,和协程配合会导致线程堆叠。推荐使用 IO 多路复用(asioboost::asio)或自定义事件循环。
  2. 销毁协程
    协程句柄不自动销毁,必须显式 handle.destroy() 或让 promise_typefinal_suspend 负责。
  3. co_returnco_yield 混用
    co_return 用于返回单一结果;co_yield 用于生成序列。不要在同一协程里混用,否则会导致逻辑混乱。
  4. 异常安全
    await_resume 可以抛异常;协程异常在 promise_type::unhandled_exception() 处处理。若不处理,程序会直接 std::terminate()

7. 结语

C++20 的协程为异步编程带来了前所未有的简洁性和性能。它们既不是回调也不是线程,而是一种状态机,能够在保持同步风格的同时,隐藏掉异步实现细节。掌握协程的基本原理、awaitable 设计以及正确的资源管理,你就能在高并发网络服务、游戏引擎、实时数据处理等领域写出更可维护、更高效的代码。

欢迎大家继续深入探索协程与 asiostd::generatorstd::task 的组合使用,开启 C++20 异步编程的新篇章。

发表评论