C++20协程

前言

之前看项目碰到了用协程实现的线程池调度系统,看的人一脸懵,我做项目都是基于Qt框架的吗,习惯于要做什么任务,就把任务丢进任务队列,调用的是qt自带的全局线程池,同事实现的线程池可以自定义线程名,写的挺高级的,就到现在我老想着掌握协程。

协程的运行机制

协程分为有栈协程和无栈协程,这个就不多说了,我们常用的c++协程就是无栈协程,其实可以理解为他是一个特殊的函数,普通的函数运行到底就结束了,线性执行,协程可以挂起中断,外部处理完了再resume唤醒执行下一步。

协程的组成

协程状态coroutine state

堆上new一个空间,存放协程的状态。状态存放的是协程运行的参数变量,运行的状态,挂起的断点等,由系统管理。

承诺对象promise

承诺体的表现形式为return::promise_type,返回值为return。

承诺对象结构体内要实现若干接口,用于辅助协程,构造协程函数返回值,提交co_yield,co_return返回值。明确协程启动阶段是否挂起以及异常处理。

接口如下

  • get_return_object()用于生成协程函数的返回对象
  • initial_suspend()用于明确初始化后协程函数的执行行为,返回等待体(awaiter),用co_wait调用其返回值,如果是std::suspend_always,表示初始化后直接挂起,要执行协程函数体要用resume唤醒协程。如果返回的是std::suspend_never,表示初始化后就直接开始执行协程函数。
  • return_value(T v)用co_return会调用这个函数,可以保存co_return的返回结果。
  • yield_value(T v)用co_yield会调用这个函数,可以保存co_yield的返回结果,返回std::suspend_always会挂起,返回std::suspend_never不会挂起。
  • final_suspend() noexcept 协程的退出接口,调用以后如果返回suspend_never,会销毁生成器对象如果返回的是suspend_always,则需要手动调用销毁句柄的handle.destroy()销毁,注意返回suspend_always不会挂起协程。

协程句柄(coroutine handle)

协程句柄是协程的标识,用于协程的挂起恢复,销毁的句柄。

表现形式是std::coroutine_handle<promise_type>,其模板参数为承诺体类型,句柄有几个重要函数。

  • resume()唤醒协程
  • done()判断协程是否已经完成,返回false表示还没有完成。
  • std::coroutine_handle<promise_type>::from_promise通过promise获取其句柄
  • std::coroutine_handle<promise_type>::promise()通过句柄获取其承诺体对象promise

等待体(awaiter)

co_wait()会调用一个等待体对象awaiter,awaiter内部有3个接口,具体做什么要根据co_wait决定。

bool await_ready()等待体是否准备好了。返回false表示没有准备好,表示协程还没准备好,立即调用await_suspend。返回true表示准备好了。

auto await_suspend(std::coroutine_handle<>handle)如果要挂起调用这个接口,handle参数就是调用等待体的协程,返回参数有3种

  • bool true 立即挂起,false不挂起
  • void同上面的true
  • 协程句柄 立即恢复句柄对应的协程运行

auto await_resume()协程恢复时调用的接口,返回值作为co_wait的返回值。

等待体的特化类型

等待体有两个特化类型

  • std::suspend_never不挂起的特化等待体类型
  • std::suspend_always挂起的特化等待体类型

例子

例1

使用co_yield的方式逐步打印5个数字,初步理解协程的写法

  1. 定义了一个模板类 Generator<T>该类用于生成一系列类型为 T 的值。它内部实现了协程相关的 promise_type,并通过 std::coroutine_handle 管理协程的生命周期。
  2. 实现了 next() 方法该方法用于获取下一个生成的值。如果协程还未结束,则恢复协程并返回当前值,否则返回 std::nullopt。
  3. 定义了一个生成器函数 counter(int max)这是一个协程函数,使用 co_yield 依次生成从 0 到 max-1 的整数。
  4. 主函数 main()
  5. 创建一个生成器 counter(5),用于生成 0 到 4 的整数。
  6. 通过循环调用 gen.next(),每次获取一个值并输出,直到生成器结束。
cpp 复制代码
#include <coroutine>
#include <iostream>
#include <optional>

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

    struct promise_type {
        T current_value;
        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
        Generator get_return_object() {
            return Generator{ handle_type::from_promise(*this) };
        }
    };

    handle_type coro;

    Generator(handle_type h) : coro(h) {}
    Generator(const Generator&) = delete;
    Generator& operator=(const Generator&) = delete;
    Generator(Generator&& other) noexcept : coro(other.coro) { other.coro = nullptr; }
    ~Generator() { if (coro) coro.destroy(); }

    std::optional<T> next() {
        if (!coro.done()) {
            coro.resume();
            if (!coro.done())
                return coro.promise().current_value;
        }
        return std::nullopt;
    }
};

Generator<int> counter(int max) {
    for (int i = 0; i < max; ++i) {
        co_yield i;
    }
}

int main() {
    auto gen = counter(5);
    while (auto v = gen.next()) {
        std::cout << "Value: " << *v << std::endl;
    }
    return 0;
}

例2

这个是用co_wait模拟异步等待

• SimpleAwaiter 是一个自定义的可等待对象,实现了 await_ready、await_suspend 和 await_resume。

• async_func 是一个协程函数,使用 co_await 等待 SimpleAwaiter 完成,并获取结果。

• Task 是协程的返回类型,简化实现,仅用于演示。

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

// 简单的可等待对象
struct SimpleAwaiter {
    bool await_ready() const noexcept { return false; } // 是否准备好,false表示需要挂起
    void await_suspend(std::coroutine_handle<>h) const noexcept {
        std::cout << "协程挂起,等待事件完成..." << std::endl;
		h.resume(); // 立即恢复协程
    }
    int await_resume() const noexcept { // 唤醒后返回的值
        std::cout << "协程恢复,事件完成!" << std::endl;
        return 42;
    }
};

// 协程返回类型
struct Task {
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

// 协程函数,使用 co_await
Task async_func() {
    int result = co_await SimpleAwaiter{};
    std::cout << "co_await 得到结果: " << result << std::endl;
}

int main() {
    async_func(); // 启动协程
    return 0;
}

例3

很多博客说协程不可以在另一个线程恢复,这个是错的,但实现前提要自己保证线程安全,这里演示跨线程唤醒协程

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

// 可等待对象,每次 co_await 都会在新线程恢复协程
struct ThreadResumeAwaiter {
    int count;
    ThreadResumeAwaiter(int c) : count(c) {}

    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> h) const {
        std::jthread([h, c = count] {
            std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟异步
            std::cout << "[线程 " << std::this_thread::get_id() << "] 唤醒协程,第 " << c << " 次" << std::endl;
            h.resume();
            }).detach();
    }
    void await_resume() const noexcept {}
};

struct Task {
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

Task async_func() {
    for (int i = 1; i <= 3; ++i) {
        std::cout << "[线程 " << std::this_thread::get_id() << "] 协程挂起,第 " << i << " 次" << std::endl;
        co_await ThreadResumeAwaiter(i);
    }
    std::cout << "[线程 " << std::this_thread::get_id() << "] 协程结束" << std::endl;
}

int main() {
    async_func();
    // 主线程等待所有子线程完成
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 0;
}

这就是协程基础,那些高级应用我再研究研究。

相关推荐
C++实习生6 小时前
Visual C++ 2005 Express 中文版
express·c++20
Ethan Wilson2 天前
VS2019 C++20 模块相关 C1001: 内部编译器错误
开发语言·c++·c++20
DYS_房东的猫2 天前
《 C++ 零基础入门教程》第10章:C++20 核心特性 —— 编写更现代、更优雅的 C++
java·c++·c++20
zFox8 天前
三、Kotlin协程+异步加载+Loading状态
kotlin·android jetpack·协程
一只一只10 天前
Unity之协程
unity·游戏引擎·协程·coroutine·startcoroutine
ice_junjun13 天前
C++20 线程返回值处理指南
c++20·c++ 多线程返回值
七夜zippoe15 天前
Python异步编程基石:深入理解asyncio核心原理与实战
python·async·协程·同步·异步·await
凌乱风雨121116 天前
从源码角度解析C++20新特性如何简化线程超时取消
前端·算法·c++20
天然玩家21 天前
【计算机技术】线程/协程/纤程/虚拟线程
线程·纤程·协程·虚拟线程