掌握C++17中的协程:从概念到实战

C++17 在标准库中引入了协程(Coroutines),为异步编程和延迟计算提供了更直观、更高效的工具。协程的核心思想是让函数能够在执行过程中暂停,并在需要时恢复,保持本地状态而不必将整个函数堆栈压栈。本文将从协程的基本概念、关键类型、实现原理、常见使用场景以及一个完整的实战示例来系统阐述。


1. 协程的基本概念

协程是可以在多次调用间保持其执行状态的函数。与传统函数相比,协程可以在任意点 co_awaitco_yieldco_return 暂停,然后在后续继续执行。其关键特性包括:

  • 挂起点co_awaitco_yieldco_return 三种挂起点。
  • 状态保持:协程内部的局部变量在挂起后仍保持其值。
  • 控制流:协程返回一个 promise 对象,调用者可通过该对象获取结果或等待完成。

协程让异步逻辑可以像同步代码一样书写,显著降低复杂度。


2. 关键类型与语法

C++协程主要涉及以下类型:

类型 说明
std::suspend_always 永远挂起,常用于测试或实现自定义调度器。
std::suspend_never 从不挂起,常用在非挂起的场景。
std::coroutine_handle<> 用于操作协程的句柄,能够 resumedestroy 等。
`std::generator
| C++23 标准提供的生成器,类似yield` 的功能。

语法示例

#include <iostream>
#include <coroutine>
#include <exception>

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

    std::coroutine_handle <promise_type> handle;
    explicit my_task(std::coroutine_handle <promise_type> h) : handle(h) {}
    ~my_task() { if (handle) handle.destroy(); }
    void start() { if (handle) handle.resume(); }
};

my_task example() {
    std::cout << "Step 1\n";
    co_await std::suspend_always{};
    std::cout << "Step 2\n";
    co_return;
}

int main() {
    auto t = example();
    t.start(); // Step 1
    t.start(); // Step 2
}

上述代码展示了一个最小的协程实现:协程在 co_await 处挂起,随后再次 resume 继续执行。


3. 协程实现原理

C++ 协程在编译时被转换为一个状态机。编译器会:

  1. 生成结构体:包含协程内部状态(例如 current_state、局部变量)。
  2. 生成 promise_type:实现协程的生命周期接口。
  3. 生成 resume 方法:根据 current_state 跳转到相应位置。
  4. 挂起点:在 co_awaitco_yieldco_return 位置插入状态改变代码。

状态机转换表(简化):

当前状态 代码行 下一状态
0 co_await std::suspend_always{} 1
1 std::cout << "Step 2" 2
2 co_return Finished

4. 常见使用场景

场景 协程优势
异步 I/O 通过 co_await 让 I/O 完成后再恢复,代码更像同步。
数据流 使用 `generator
` 逐个生成值,内存占用低。
游戏循环 每帧挂起/恢复状态,避免繁琐的状态机代码。
协程调度 在自定义调度器中挂起/恢复,支持优先级调度。

5. 真实案例:使用协程实现异步文件读取

下面演示如何使用 co_await 与标准库中的 std::future 搭配,实现一个简单的异步文件读取器。

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

struct async_file_reader {
    struct promise_type {
        std::future<std::string> get_return_object() {
            return std::async(std::launch::async, [this]() { return std::move(result); });
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { std::terminate(); }
        void return_value(std::string&& str) { result = std::move(str); }

        std::string result;
    };

    std::future<std::string> fut;
    explicit async_file_reader(std::future<std::string> f) : fut(std::move(f)) {}
};

async_file_reader read_file_async(const std::string& path) {
    std::ifstream file(path);
    std::string content((std::istreambuf_iterator <char>(file)),
                        std::istreambuf_iterator <char>());
    co_return std::move(content);
}

int main() {
    auto reader = read_file_async("example.txt");
    // 这里可以继续执行其他任务
    std::cout << "File read in background.\n";
    // 等待结果
    std::string data = reader.fut.get();
    std::cout << "File content:\n" << data << '\n';
}
  • async_file_reader 封装了协程,返回一个 std::future,允许在外部继续执行其他逻辑。
  • read_file_async 在后台读取文件,挂起期间不阻塞主线程。

6. 协程调度器:自定义事件循环

协程本身不包含调度逻辑,通常与事件循环结合使用。以下是一个简易的事件循环示例,支持多协程挂起/恢复。

#include <queue>
#include <functional>
#include <coroutine>

class EventLoop {
public:
    using Task = std::coroutine_handle<>;

    void push(Task t) { tasks.push(t); }
    void run() {
        while (!tasks.empty()) {
            Task t = tasks.front(); tasks.pop();
            if (!t.done()) t.resume();
            else t.destroy();
        }
    }
private:
    std::queue <Task> tasks;
};

EventLoop loop;

Task timer_task(std::chrono::milliseconds ms) {
    std::cout << "Timer started\n";
    co_await std::suspend_always{};
    std::this_thread::sleep_for(ms);
    std::cout << "Timer finished\n";
}

int main() {
    loop.push(timer_task(1000));
    loop.run();
}

此处 timer_task 在挂起点等待,事件循环恢复后执行后续代码。通过将挂起点与回调相结合,可实现复杂的协程调度。


7. 常见坑与最佳实践

  1. 忘记销毁协程句柄:未销毁会导致内存泄漏,最好使用 RAII 包装。
  2. 过度使用 co_await std::suspend_always{}:在生产环境中,应使用真正的异步操作或自定义调度器。
  3. 与异常结合:在 promise_type::unhandled_exception 中捕获并处理,否则会直接 terminate()
  4. 性能开销:协程状态机会增加字节码,使用 std::suspend_alwaysstd::suspend_never 进行微调。
  5. 跨平台支持:C++17 协程已在大多数主流编译器实现,但仍需确认目标平台的实现细节。

8. 小结

  • 协程让异步代码更易读、易维护。
  • C++17 引入协程核心原语,C++20 对标准库进一步完善。
  • 通过 co_awaitco_yieldco_return 挂起点实现状态机。
  • std::future、自定义事件循环配合,可构建高效的异步框架。
  • 谨慎管理协程生命周期,避免资源泄漏。

掌握协程后,你将能够轻松实现高并发 I/O、流式数据处理以及复杂的游戏逻辑。继续深入阅读标准库文档,实践各种场景,相信你能在 C++ 编程中驾驭协程的强大力量。

发表评论