C++17中的协程(coroutines)实战指南

在 C++20 之前,协程(coroutines)在 C++ 标准库中并未正式加入,但借助 GCC 和 Clang 的实验性扩展,开发者已经可以在 C++17 环境下实现协程功能。本文将介绍协程的基本概念、如何在 C++17 环境下使用它们,以及一个简单的生产者-消费者示例。

1. 协程基础

协程是可暂停的函数,能够在执行过程中多次挂起并恢复,从而实现异步编程、生成器等功能。相比传统的线程,协程占用更少资源,并且可以在单线程内完成多任务协作。

在 C++20 标准中,协程语法通过 co_await, co_yieldco_return 三个关键字实现。然而在 C++17 的实验性实现中,可以使用 std::experimental::coroutine 提供的 API 来手动编写协程。

2. 环境准备

  • GCC 9+ 或 Clang 10+:这两个编译器已实现 std::experimental::coroutine
  • 编译选项:在编译时需要开启实验性协程支持:
    g++ -std=c++17 -fcoroutines -Wall -O2 main.cpp -o main

    或者

    clang++ -std=c++17 -fcoroutines -Wall -O2 main.cpp -o main

3. 关键组件

3.1 promise_type

每个协程都有一个对应的 promise_type,用于管理协程的生命周期和返回值。示例 generator

template<typename T>
struct generator {
    struct promise_type;
    using handle_type = std::experimental::coroutine_handle <promise_type>;

    struct promise_type {
        T current_value;
        std::exception_ptr exception;

        generator get_return_object() {
            return generator(handle_type::from_promise(*this));
        }
        std::experimental::suspend_always initial_suspend() { return {}; }
        std::experimental::suspend_always final_suspend() noexcept { return {}; }
        std::experimental::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }
        void unhandled_exception() { exception = std::current_exception(); }
        void return_void() {}
    };

    handle_type coro;
    generator(handle_type h) : coro(h) {}
    ~generator() { if (coro) coro.destroy(); }

    struct iterator {
        handle_type coro;
        bool done;

        iterator(handle_type h, bool d) : coro(h), done(d) {}

        iterator& operator++() {
            coro.resume();
            done = !coro.done();
            return *this;
        }
        T operator*() const { return coro.promise().current_value; }
        bool operator==(std::default_sentinel_t) const { return done; }
    };

    iterator begin() {
        coro.resume();
        return iterator(coro, coro.done());
    }
    std::default_sentinel_t end() { return {}; }
};

3.2 使用协程

下面的例子演示了一个简单的整数生成器:

generator <int> count_to(int n) {
    for (int i = 1; i <= n; ++i) {
        co_yield i;   // 挂起并返回当前值
    }
}

在主函数中可以像遍历普通容器一样使用:

int main() {
    for (auto x : count_to(5)) {
        std::cout << x << " ";
    }
    // 输出: 1 2 3 4 5
}

4. 生产者-消费者示例

下面给出一个使用协程实现的生产者-消费者模型,展示协程与同步机制的结合。

#include <iostream>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <experimental/coroutine>

using namespace std::experimental;

// 生产者生成整数
generator <int> producer(int count, int delay_ms) {
    for (int i = 0; i < count; ++i) {
        // 模拟耗时工作
        std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
        co_yield i;   // 产生一个值
    }
}

// 消费者从队列读取
void consumer(std::queue <int>& q, std::mutex& m, std::condition_variable& cv, bool& done) {
    while (true) {
        std::unique_lock<std::mutex> lock(m);
        cv.wait(lock, [&]{ return !q.empty() || done; });

        while (!q.empty()) {
            int val = q.front(); q.pop();
            std::cout << "Consumed: " << val << std::endl;
        }

        if (done) break;
    }
}

int main() {
    std::queue <int> q;
    std::mutex m;
    std::condition_variable cv;
    bool done = false;

    // 启动消费者线程
    std::thread consumer_thread(consumer, std::ref(q), std::ref(m), std::ref(cv), std::ref(done));

    // 生产者协程
    for (int val : producer(10, 100)) {
        {
            std::lock_guard<std::mutex> lock(m);
            q.push(val);
        }
        cv.notify_one();
    }

    // 通知消费者结束
    {
        std::lock_guard<std::mutex> lock(m);
        done = true;
    }
    cv.notify_one();

    consumer_thread.join();
    return 0;
}

5. 小结

  • C++17 已经通过实验扩展提供了协程支持,使用 -fcoroutines 开关即可启用。
  • 关键在于实现 promise_type 并掌握 co_yield/co_return 的使用方式。
  • 协程非常适合实现生成器、事件循环和轻量级异步任务,可显著简化代码逻辑。
  • 在实际项目中,可以与线程、同步原语结合,构建高性能、可维护的异步框架。

希望本文能帮助你在 C++17 环境中顺利使用协程,开启更高效的编程之路。

发表评论