C 20 协程初探

【导读】:C 20 终于引入了协程特性,给库作者提供了一个实现协程的机制,让用户方便使用协程来编写异步逻辑,降低了异步并发编程的难度。结合我最近协程的学习,在这里记录一下相关内容。

以下是正文


使用场景

协程和普通函数相比,多了个中途随时 挂起 ,随后 恢复 的过程,当用户调用一个阻塞请求接口,从而让出控制权,当响应时,恢复之前的控制流,从而大大提高线程复用率,这也注意了协程只是并发的,并不是真正意义上的并行,在 IO 密集型场景下,协程能够很好的提高资源利用率,用少数的线程达到并发成百上万个协程的效果。

而相对传统的线程池 回调模式,每发起一个请求,为了避免阻塞当前线程,需要挂一个回调函数处理后续过程,而回调函数又可能产生竞争,导致得加锁处理。而协程却能够以同步方式写实现异步,后续过程直接挂起,当响应的时候恢复执行。

我参与的项目中,对象随时都可能起个线程干活,或者常驻于对象生命周期里,统计下来整个项目居然开了几百个线程,由于多线程编程难免导致竞争,从而需要锁这种很低级的机制做同步,而一旦引入了锁,就不可避免的扩散开来,大家看到这里加把锁,那我也加把锁,统计下来代码里面居然也有几百把锁。真是维护的噩梦。

由于协程能够随时挂起,后续恢复,这就能实现一些延迟计算的特性,例如生成器。

扯远了,本文主题是关于 C 20 的协程,在 C 20 还没稳定之前,先来学习一下相关知识,读完本文后你应该能利用这个机制实现一些想要的协程了。

概念模型

C 20 的协程设计为无栈协程,相对于有栈协程,省掉了上下文切换开销[1],只能手动切换,效率更高,也不用管理复杂的寄存器状态,移植性更好,但这同时也导致了不能被非协程函数嵌套调用。

同时引入了 3 个关键字:

1. co_yield: 挂起并返回值

2. co_await: 挂起

3. co_return: 结束协程

当一个函数出现了上面的关键字,则该函数是个协程。


Promise

当 caller 调用一个 callee 协程的时候,协程自身的状态信息 [2](形参,局部变量,自带数据,各个阶段点执行点)会被保存在堆上的 Promise 对象中,这也是编译器会在协程里面插入 Promise 相关代码,以及一些执行点。由于 Promise 的大小可以在编译期计算出来,从而避免了内存浪费。而 Promise 对象所有权可由coroutine_handle 句柄持有。

Future

而 Future 对象主要是与 Promise 对象交互的桥梁,既 caller 与 callee 之间的通信:

1. callee 挂起时,将值返回给 caller: yield 语义

2. callee 执行结束时,将值返回给 caller: return 语义

3. callee 恢复时,caller 将值带给 callee

需要注意的是,这些概念和标准库的 std::promise/std::future 不是同一个东西,后者用于做同步用,std::future会阻塞等待直到 std::promise 提供值,可以看做是条件变量的封装,同样地,和其他语言的 Promise/Future 概念也不一样。

Awaitable

如果一个对象是 Awaitable 对象,那么可以用 co_await 操作符去触发该对象的动作 ready/suspend/resume,从而转移、恢复控制权,co_await 细节留到后面在介绍。

具体机制

了解了概念模型后,我们可以进一步探讨背后的机制了。

Promise/Future 对象

当一个协程被调用时,会创建 Promise 对象,然后编译器会在各个阶段插入一些代码[3]:

{  co_await promise.initial_suspend();  try  {      }  catch (...)  {    promise.unhandled_exception();  }FinalSuspend:  co_await promise.final_suspend();}

可以看到一个协程函数,分为如下几个步骤:

1. 从堆上 (operator new) 创建 Promise 对象,保存协程的状态信息

2. initial_suspend 阶段,用于在执行协程主体  代码前做些事情

3. 阶段,执行协程的主体代码

4. unhandled_exception 阶段,若抛异常,处理异常

5. final_suspend阶段,协程结束收尾动作,在这阶段的 coroutine_handle::done 方法为 true,caller 可以通过这个方法判断协程是否结束,从而不再调用 resume 恢复协程。

而协程返回类型则是一个 Future 对象,这一步编译器通过 Promise::get_return_object() 来创建 Future 对象。而 Future 对象一般持有 Promise 的句柄:coroutine_handle,这样 caller 可以通过 Future 与 Promise 交互,从而恢复协程。

而 Promise 对象释放的时间点有两个,避免重复执行,否则会 double free:

1. final_suspend 阶段 resume 后

2. 调用 coroutine_handle::destroy() 方法

比较好的做法是在 final_suspend 阶段挂起,这时候就不可 resume 了,在 caller 通过调用 Future 持有的句柄 destroy() 方法释放 Promise 对象。综上,一个 Promise 对象需要实现如下方法:

1. initial_suspend: 返回一个 Awaitable 对象

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值