【C++新特性】:谈谈C++20的协程(10000字讲清楚C++协程)

协程

协程这个概念对于计算机编程语言来说是并不陌生的,许多编程语言都支持协程,比如C# Python KotlinC++C++20正式在标准里面引入了协程的概念,但是从C++20的角度来看官方只是提供了支持协程的框架,具体的实现还是得依赖于程序员本身,今天,我就官方如何针对协程进行描述,以及如果个性化设计满足当前项目的协程进行一些讨论,如果有说的不对或者不完整的地方,欢迎留言讨论。

什么是协程

一种轻量级别的线程
对于程序员来说,我们可以将协程理解成为一个对于系统级别线程来说更加轻量的应用级别的线程,他主要是为了解决异步编程复杂度的问题。他可以挂起和恢复
在这里插入图片描述

为什么要设计协程

如果有做过网络异步框架设计的读者应该能够明白,对于传统的异步框架,最经典和常用的方式就是注册回调函数**(比如经典的Boost.Asio),通过注册回调函数的方式,让我们的操作可以在另一个或几个执行流里面去执行,但是这里面的复杂程度还是困难的,我们需要管理各种变量的生命周期**,由于是异步编程,因此不恰当的管理,很可能导致悬空的问题,当然,较好的方式就是通过智能指针通过RAII进行生命周期管理,但是这对于我们程序员的要求很大,而且,对于网络IO来说,read_async必须在write_async之前,write_async可能还要通知进行read_async, 合理管控他们之前的调度顺序以及调度逻辑也是复杂的,因此协程应运而生了。

传统的异步框架

// 里面还有很复杂的管理逻辑, 这里只是简单抽象
server.async_connect([server]()
	{
		server.async_read([server]()
		{
			server.async_send()
		})
	})

协程

task<void> func()
{
	auto connect = co_await async_connect();
	auto read_msg = co_await async_read(connect);
	// 得到send_msg;
	auto send_msg = handle(read_msg);
	auto sucess = co_await async_send(connect, send_msg);
}

这么一看,无论是在异步的设计复杂上,以及代码的可读性,协程都优于传统的回调函数。

C++20的协程

首先,协程分为有栈协程和无栈协程两种

有栈协程

首先,协程代表着一个任务,有栈协程会为每一个协程分配一个独立的,固定大小的栈类似操作系统的线程,拥有独立的栈空间,我们可以通过汇编层直接转换调用栈,进行协程的切换。

类比

比如我有很多录像带(协程栈), 我想要在电视(操作系统)看这几部片子(协程), 我们的录像带会帮助我们记录我们看到哪里了,下次我们可以在观看任意片子的时候,暂停,然后切换成另外一个片子上次看到的位置。

特点
  • 独立性: 每个协程有自己独立的栈空间,互不干扰。

  • 实现简单: 对编程语言来说,实现起来相对直观,因为它的栈模型和线程类似。

  • 阻塞是灾难: 如果在协程中调用了阻塞式I/O操作(如sleep, read),整个线程都会被阻塞,该线程下的所有其他协程都无法执行。因此,必须配合非阻塞I/O和事件循环。

  • 栈大小固定: 栈空间大小需要预先分配,分配过小可能导致栈溢出,分配过大则浪费内存。

  • 上下文切换开销较大: 需要保存和恢复整个栈,虽然比线程切换轻量,但相比无栈协程开销更大。

无栈协程

无栈协程没有自己独立的调用栈。它的状态保存不依赖于一个完整的栈,而是通过状态机和闭包(或生成器) 来实现。操作系统会分析协程函数,将所有的局部变量放到我们的堆上(协程帧),他只需要在堆上维护这个协程帧即可,协程函数会被编译成为一个状态机, 根据当前的状态跳转到函数的对应的位置。

类比
我在看一本小说小说(状态机), 从中我记录了很多的位置(第40页第5行页, 第100页第4行(await点)),我们只需要保存我们是当前的书签(状态和变量),我们可以把书合上**(挂起)**,在任何地方,在图书馆,在家里(线程)我都可以打开我们的书,通过我们的书签很轻易的恢复他。

  • 极致轻量: 由于没有栈的分配和切换,创建和切换开销极小,可以轻松创建百万甚至千万级别的协程。

  • 内存效率: 只保存必要的变量,内存占用非常小。

  • 与语言深度集成: 需要编译器的强大支持来实现状态机的转换。

  • 传染性: 一个函数如果内部有await,它本身也必须被标记为async,这会导致调用链上所有函数都变成async(即“颜色”问题)。

  • 不能随意挂起: 只能在明确的await点挂起,而不能在嵌套的普通函数中随意挂起。

C++20如何定义一个协程

首先简单来讲,协程就是一个函数,一个可以挂起可以恢复的函数, 方便后面的学习,必须牢记这段话。

promise_type

首先明确,这里的promise_type和C++11里面的std::promise没有任何的关系,就像老婆和老婆饼的关系一样,promise_type的是协程的控制中心,它里面必须定义存储的返回值类型,也就是你这个任务的结果对象。

必须定义的接口

  • initial_suspend : 我们这个协程创建的时候, 定义协程的状态
    • std::suspend_always : 创建立即挂起,等外界进行恢复。
    • std::suspend_never : 创建好不挂起,直接运行。
  • unhandled_exception :处理异常
  • final_suspend : 同样的道理,协程任务结束后,是挂起还是直接释放,这也是取决于返回suspend_always还是suspend_never。
  • return_void / return_value : 我们前面提到promise_type中存储了任务的结果对象,当我们在协程任务重调用co_return的时候,会自动调用return_void / return_value,如果我们的任务没有结果,就实例化一个return_void接口,反之实例化return_value, 根据我们需求进行定制化,当然我们也可以使用模版特化。
  • yield_value: 当协程任务调用co_yield的时候调用这个接口。
  • Task get_return_object : promise_type定义了定义了协程的信息,我们需要定制化这个接口,方便外部句柄可以访问promise_type对象。

std::coroutine_handle

协程的句柄, 如果 promise_type 是协程的"大脑",那么 std::coroutine_handle 就是协程的遥控器。它是从外部与挂起协程交互的主要接口,提供了控制协程执行和访问其内部状态的能力。

  • std::coroutine_handle::resume : 恢复协程
  • std::coroutine_handle::destroy : 销毁协程帧,如果promise_type::final_suspend的返回值是suspend_always的时候可以调用。
  • std::coroutine_handle::done : 协程任务是否已经结束。
  • std::coroutine_handle::from_promise : 和get_return_object配合使用,让我们在promise内部可以通过promise直接创建一个句柄,也就是我们可以通过空调(promise)定制化创建他的遥控器(handle)给我们的人(Task)

任务(Task)

有了大脑,但是没有身体也是不行的,我们需要根据我们的需求定制一个能够装的下我们这个大脑的躯体,也就是容器。

这里我们提供几种常见的协程设计模式

RAII管理句柄
template <typename promise_type>
    class CoroHandleGuard
    {
        using handle_t = std::coroutine_handle<promise_type>;

        handle_t handle_;

    public:

        CoroHandleGuard(handle_t handle) : handle_(std::move(handle)) {}

        CoroHandleGuard(std::nullptr_t) : handle_(nullptr) {}

        CoroHandleGuard() = default;

        // delete Constructors and assignment operators
        CoroHandleGuard(const CoroHandleGuard&) = delete;

        CoroHandleGuard& operator=(const CoroHandleGuard&) = delete;
        // 
        CoroHandleGuard(CoroHandleGuard&& other) noexcept 
            : handle_(Tools::exchange(other.handle_, nullptr)) 
        {}

        promise_type& promise()
        {
            return handle_.promise();
        }

        void resume() const noexcept
        {
            handle_.resume();
        }
        bool done() const noexcept
        {
            return handle_.done();
        }
        // detach the coroutine handle and return it
        // give the ownership of the coroutine to the caller
        std::coroutine_handle<> detach()
        {
            return Tools::exchange(handle_, nullptr);
        }

        std::coroutine_handle<> raw_handle() noexcept
        {
            return handle_;
        }

        ~CoroHandleGuard()
        {
            if (handle_ != nullptr)
                handle_.destroy();
        }
    };
Lazy Task

和懒汉模式的思想很像,调用时候执行

  1. 定义Lazy Task的promise_type
    template <typename T>
    class LazyFunctionPromise
    {
    public:
        using handle_t = std::coroutine_handle<LazyFunctionPromise<T>>;
        LazyFunction<T> get_return_object() noexcept
        {
            return LazyFunction<T>{ handle_t::from_promise(*this)};
        }
        std::suspend_always initial_suspend() const noexcept
        {
            return {};
        }
        std::suspend_always final_suspend() const noexcept
        {
            return {};
        }
    
        void unhandled_exception()
        {
            std::terminate();
        }
    
        void return_value(const T& value)
        {
            result_ = value;
        }
    
        const T& result() const { return result_; }
    
        T& result() { return result_; }
    private:
        T result_;
    
    };
    
  2. 定义Lazy Task任务类型
    template <typename T>
    class LazyFunction
    {   
    public:
        using promise_type = LazyFunctionPromise<T>;
        using handle_t = promise_type::handle_t;
    
        LazyFunction(handle_t handle) : handle_(handle) {}
    
        T get()
        {
            if(not handle_.done())
                handle_.resume();
            return handle_.promise().result();
        }
        
        operator T() { return get(); }
    
    private:
        CoroHandleGuard<promise_type> handle_;
    };
    
  3. 测试用例**(可以看我写的执行顺序)**
    LazyFunction<int> lazy_task()
    {
    	// 1. 创建好promise_type 通过 get_return_object实例化LazyFunction
    	// 2. suspend_always直接挂起这个协程
    	// 
    	// 4. 协程被reume(), 继续往下执行
    	co_return 30;  // 5. 调用set_value() 将result_设置为30
    	
    }
    auto main() -> int
    {
    	// 注意main函数不能声明成协程
    	auto lf = lazy_task();
    	std::cout << lf << std::endl;
    	// 3. 调用lf.operator int类型, 将挂起的协程resume()
    	// lf.get() == promise::result_; // 所以得到了30
    }
    
Generator

给你一个场景,我希望返回值是可以迭代的,这有很多种设计的方式,比如我可以将返回值设置成可迭代的对象,比如std::vector之类的序列化容器,但是这样会很浪费空间而且很重,因为我们需要在函数内部将数据拷贝进入容器中,然后进行返回,而且,如果我希望这个接口是可以无限迭代的?(举个例子),很多读者可能会想到Python的yield, 对,就是这个,因为Python也是支持协程的

def get_some_random(int count):
	for i in range(count):
		yield create_random()

def main():
	for i in get_some_random(6):
		print(i)
	

话题回过来,C++我们该怎么做?这个时候就是promise_type中的co_yield发力的时候了。

  1. 定义promise_type
    template <typename T>
    class GeneratorPromise
    {
    public:
        using handle_t = std::coroutine_handle<GeneratorPromise<T>>;
        Generator<T> get_return_object() noexcept
        {
            return Generator<T>{ handle_t::from_promise(*this)};
        }
        std::suspend_always initial_suspend() const noexcept
        {
            return {};
        }
        std::suspend_always final_suspend() const noexcept
        {
            return {};
        }
    
        void unhandled_exception()
        {
            std::terminate();
        }
    
        std::suspend_always yield_value(const T& value)
        {
            result_ = value;
            return {};
        }
    
        void return_void()
        {
            result_ = T{};
        }
    
        const T& result() const { return result_; }
    
        T& result() { return result_; }
    private:
        T result_;
    
    };
    
  2. 定义迭代器(我们希望这个Generator是可以迭代的)
    template <typename T>
    class GeneratorIterator
    {
    public:
        using self = GeneratorIterator<T>;
        using ref = self&;
        using const_ref = const self&;
    
    
        using promise_type = GeneratorPromise<T>;
        using handle_t = std::coroutine_handle<promise_type>;
    
        GeneratorIterator(CoroHandleGuard<promise_type> handle) : handle_(std::move(handle)) {}
    
        GeneratorIterator(self&& other) noexcept : handle_(Tools::exchange(other.handle_, nullptr)) {}
    
        const T& operator*() const { return handle_.promise().result(); }
        
        T& operator*() { return handle_.promise().result(); }
    
        ref operator++() {
            assert(not handle_.done());
            handle_.resume();
            return *this;
        }
        
        void operator++(int){
            ++*this;
        }
        
    
        ref operator=(self&& other) noexcept{
            handle_ = Tools::exchange(other.handle_, nullptr);
            return *this;
        }
        // end() of the range
        friend bool operator==(const_ref a, std::default_sentinel_t){
            return a.handle_.done();
        }
    
    private:
        CoroHandleGuard<promise_type> handle_;
    };
    
  3. 定义Generator(任务)
    template <typename T>
    class Generator {
        
    public:
        // Store the coroutine handle
        using promise_type = GeneratorPromise<T>;
        using handle_t = std::coroutine_handle<promise_type>;
    
        using iterator = GeneratorIterator<T>;
    
        Generator(handle_t handle)
        : handle_(handle) 
        {}
        
        // Let the coroutine generate the next result if it's not yet done
        bool exhausted() {
            if (not handle_.done())
                handle_.resume();
            return handle_.done();
        }
    
        T get() {
            return handle_.promise().result_;
        }
    
        iterator begin()
        {
            handle_.resume();
            return iterator(std::move(handle_));
        }
    
        std::default_sentinel_t end()
        {
            return {};
        }
    
        ~Generator() = default;
    private:
        CoroHandleGuard<promise_type> handle_;
    };
    
  4. 测试用例
    Generator<int> get_some_random(int count)
    {
    	for(int i = 0;i < count;i++)
    		co_yeild i;
    	co_return;
    }
    auto main() -> int
    {
    	auto gen = get_some_random(5);
    	for(const auto& i : gen)
    		std::cout << i << std::endl;
    	return 0;
    }
    

Awaitable(可以co_await的对象)

你以为这样就完了吗,No No No, 你没发现我们有一个关键字一直没用吗,就是co_await,这是因为我们上面的任务类型很简单,没必要也不能用,因为如果一个函数里面有co_returnco_yieldco_await,编译器就会认为他是一个协程,上面的例子中,我们的main函数,不能co_await因为main函数不能声明成协程,这是规定,所以,看看,我们都是通过mian函数获得一个Task对象,然后使用resume()的接口来唤醒这个任务,所以不涉及co_await, 那co_await是什么呢,他的作用是我们在一个协程中等待另一个协程任务。比如下面的例子

class Task;
// 定义
Task<int> func2()
{
	co_return 30;
}
Task<int> func1()
{
	auto val = co_await func2();
	co_return val;
}

int main()
{
	// main函数不能使用co_await
	auto t = func1();
	t.get(); // 内部调用了resume()
}
什么是Awaitable对象

定义了下面三个接口的就是可以co_await的对象

  • await_ready : 是否就绪
  • await_suspend : ready == false调用
  • await_resume : 返回结果

在这里插入图片描述
这里大家应该就能理解了,我们co_await一个对象,如果他的数据没有就绪,他就会在当前或者另一个执行流中进行任务处理,我们把我们的句柄通过await_suspend传递给我们co_await的对象, 让他拥有恢复我们等待状态的权利,当他处理完他的任务,调用resume就可以恢复调用他的那个协程。

注意点,await_suspend中返回句柄的那个函数定义有什么用
比如我们定义了一个Scheduler专门处理协程的调度,这样的设计可以帮助进行调度处理,其实也就是方便一点。

#include <queue>
#include <vector>

class Scheduler {
    std::queue<std::coroutine_handle<> > ready_queue;
    
public:
    struct ScheduleAwaiter {
        Scheduler& scheduler;
        
        bool await_ready() noexcept { return false; }
        
        std::coroutine_handle<> await_suspend(std::coroutine_handle<> current) noexcept {
            // 将当前协程加入调度队列
            scheduler.enqueue(current);
            
            // 返回调度器中的下一个就绪协程
            return scheduler.dequeue();
        }
        
        void await_resume() noexcept {}
    };
    
    void enqueue(std::coroutine_handle<> handle) {
        ready_queue.push(handle);
    }
    
    std::coroutine_handle<> dequeue() {
        if (ready_queue.empty()) {
            return std::noop_coroutine();  // 空操作协程
        }
        auto handle = ready_queue.front();
        ready_queue.pop();
        return handle;
    }
    
    ScheduleAwaiter schedule() {
        return ScheduleAwaiter{*this};
    }
};

Task worker(Scheduler& scheduler, int id) {
    for (int i = 0; i < 3; ++i) {
        std::cout << "Worker " << id << " 执行步骤 " << i << "\n";
        co_await scheduler.schedule();  // 主动让出CPU
    }
}

注意点: 父子协程的问题,协程中很容易忽略的一个坑,如果不注意很容易坑了老爹
在这里插入图片描述

总结

这里我浅谈了我对于C++20协程的理解,可能有问题,欢迎指正,后续如果想的话,我也会更新如果基于协程来扩展异步编程,包括扩展Boost.Asio或者基于epoll, kqueue或者ICOP做一个基于协程的异步IO事件驱动的服务器框架。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值