c++ 协程的初步理解

1. 什么是协程?

协程是一个可以挂起 (暂停执行)并在稍后恢复的函数。与普通函数不同,协程可以:

  • 在挂起时不阻塞线程
  • 保留局部状态,恢复后继续执行
  • 由开发者明确控制挂起点(co_await, co_yield, co_return

C++20通过三个新关键字和一套编译器协议支持无栈协程。

2. 协程的三大关键字

关键字 作用 示例
co_await 挂起协程,等待某个操作完成 co_await async_read(...)
co_yield 挂起并返回一个值给调用者,可多次产生值 co_yield i;
co_return 结束协程,并可选返回一个最终值 co_return result;

3. 核心机制:Promise与Awaitable协议

协程的编译行为由两个用户定义的类型决定:PromiseAwaitable

3.1 Promise 类型(promise_type

每个协程关联一个promise_type对象,它定义了协程的生命周期行为。必须在协程的返回类型中嵌套定义。

cpp 复制代码
struct ReturnObject {
    struct promise_type {
        // 必须实现的函数
        ReturnObject get_return_object();           // 创建协程的返回对象
        std::suspend_never initial_suspend();       // 协程开始时是否挂起
        std::suspend_never final_suspend() noexcept;// 协程结束时是否挂起
        void return_void();                         // co_return 无值时调用
        void unhandled_exception();                 // 异常处理

        // 可选函数(使用 co_yield 时需要)
        std::suspend_always yield_value(int value);
    };
};

调用时机(由编译器自动调用)

1.调用协程函数 → 创建promise对象 → 调用get_return_object获得返回对象。

2.立即调用initial_suspend:若返回suspend_always则协程初始挂起;否则执行函数体。

3.协程内遇到co_return → 调用return_void或return_value → 调用final_suspend。

4.异常发生时调用unhandled_exception。

例子:

cpp 复制代码
#include <coroutine>
#include <iostream>

template<typename T>
struct Generator {
    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 {}; }
        void unhandled_exception() { std::terminate(); }
        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }
        void return_void() {}
    };

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

    // 迭代器风格
    bool next() {
        if (!handle) return false;
        handle.resume();
        return !handle.done();
    }
    T value() const { return handle.promise().current_value; }
};

Generator<int> count_up_to(int max) {
    for (int i = 1; i <= max; ++i) {
        co_yield i;       // 挂起,返回 i
    }
}

int main() {
    auto gen = count_up_to(5);
    while (gen.next()) {
        std::cout << gen.value() << ' ';
    }
    // 输出: 1 2 3 4 5
}

Awaitable 类型(用于 co_await

co_await表达式期待一个可等待对象(Awaitable),该对象需实现三个函数:

cpp 复制代码
struct Awaitable {
    bool await_ready();                 // 是否需要挂起?false表示挂起
    void await_suspend(std::coroutine_handle<> h); // 挂起时执行,保存句柄等
    T await_resume();                   // 恢复后执行,返回结果
};

调用时机:

1.co_await awaitable_obj → 先调用await_ready(),若返回true则不挂起。

2.若挂起,调用await_suspend(handle),协程暂停,控制权返回调用者/调度器。

3.未来某个时刻,通过handle.resume()恢复协程 → 调用await_resume(),其返回值作为整个co_await表达式的值。

例子:

cpp 复制代码
#include <coroutine>
#include <iostream>
#include <thread>
#include <string>
#include <atomic>

// 一个简易的 awaitable 类型,用于异步读取一行
struct AsyncReadLine {
    // 协程句柄(在 await_suspend 中保存)
    std::coroutine_handle<> handle_;
    // 存放读取结果
    std::string result_;
    // 用于同步的原子标志,避免重复恢复
    std::atomic<bool> resumed_{false};

    // await_ready:返回 false 表示需要挂起
    bool await_ready() const noexcept {
        return false;   // 总是挂起,等待输入
    }

    // await_suspend:传入协程句柄,启动异步操作
    void await_suspend(std::coroutine_handle<> h) {
        handle_ = h;    // 保存句柄,以便后面恢复
        // 启动一个线程,模拟异步 IO
        std::thread([this] {
            // 阻塞读取直到换行符
            std::string line;
            std::getline(std::cin, line);
            // 存储结果
            result_ = std::move(line);
            // 恢复协程
            if (!resumed_.exchange(true)) {
                handle_.resume();
            }
        }).detach();    // 分离线程,使其独立运行
    }

    // await_resume:协程恢复后,返回读取的结果
    std::string await_resume() {
        return std::move(result_);
    }
};

// 使用协程:等待用户输入一行,然后打印
#include <coroutine>

struct Task {
    struct promise_type {
        Task 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(); }
    };
};

Task wait_and_print() {
    std::cout << "协程:请输入一行文字:";
    // co_await 一个 AsyncReadLine 对象
    std::string line = co_await AsyncReadLine{};
    std::cout << "协程:你输入的是:" << line << std::endl;
}

int main() {
    // 启动协程
    wait_and_print();

    // 主线程在协程挂起期间继续执行其他工作
    for (int i = 0; i < 10; ++i) {
        std::cout << "主线程:执行其他任务 " << i << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }

    // 等待一下,防止程序退出时协程还未完成
    std::this_thread::sleep_for(std::chrono::seconds(5));
    std::cout << "主线程结束" << std::endl;
    return 0;
}

总结

协程不是线程,它不能利用多核。实际应用中,协程通常与线程池或事件循环结合:

  1. 协程负责逻辑编排(挂起/恢复)
  2. 线程负责执行和真正的并行I/O

参考文章:https://blog.csdn.net/CD200309/article/details/157548774

相关推荐
庞轩px2 小时前
深入理解 sleep() 与 wait():从基础到监视器队列
java·开发语言·线程··wait·sleep·监视器
故事和你912 小时前
洛谷-算法1-2-排序2
开发语言·数据结构·c++·算法·动态规划·图论
白毛大侠3 小时前
理解 Go 接口:eface 与 iface 的区别及动态性解析
开发语言·网络·golang
李昊哲小课3 小时前
Python办公自动化教程 - 第7章 综合实战案例 - 企业销售管理系统
开发语言·python·数据分析·excel·数据可视化·openpyxl
Hou'3 小时前
从0到1的C语言传奇之路
c语言·开发语言
不知名的老吴4 小时前
返回None还是空集合?防御式编程的关键细节
开发语言·python
Tanecious.4 小时前
蓝桥杯备赛:Day6-B-小紫的劣势博弈 (牛客周赛 Round 85)
c++·蓝桥杯
迈巴赫车主4 小时前
蓝桥杯3500阶乘求和java
java·开发语言·数据结构·职场和发展·蓝桥杯
流云鹤4 小时前
Codeforces Round 1090 (Div. 4)
c++·算法