08|Coroutines:“零”开销降低异步编程复杂度
- 深入了解
- 翻译
- 解释
- 总结
C++20引入了协程作为解决异步编程问题的全新方案,提供了泛化的协作式多任务模型,在并发计算和高性能I/O领域有广泛应用。相较于多线程或多进程运行时,协程可以实现几乎“零”开销的代码调度。文章介绍了C++协程的定义和执行机制,以及在实践中需要实现的Promise和Awaitable接口约定。协程是一种更加泛化的概念,具有可休眠、可恢复、不基于栈实现的特点。在协程的执行过程中,协程帧被创建在堆上,通过协程句柄coroutine_handle来控制协程的状态。此外,文章还介绍了协程的调度过程,包括co_await操作符的使用和在运行时的执行过程。通过对co_await的执行过程的详细讲解,读者可以了解到协程的休眠、控制权转移和待计算值的传递等关键操作。总的来说,C++20的标准化协程为解决高性能计算问题提供了全新的思维模式,让异步编程的复杂度大幅降低,并且促使C++20成长为全新的编程模型。文章还介绍了生成器和co_yield的使用,以及对协程的接口约定进行了详细的讲解和总结。文章指出C++20中提供的coroutines较为粗糙,缺乏标准库的支持,期待更加成熟的支持会在C++26或后续标准中到来。
《现代 C++20 实战高手课》,新⼈⾸单¥59
全部留言(7)
- 最新
- 精选
- Family mission作者你好,感觉你讲的这节内容干货满满,请解释下这个代码块的含义 template struct promise; template struct Generator : std::coroutine_handle> { using promise_type = promise;}; using promise_type = promise<T>;这个代码块的含义么,可以理解成是将模版类赋值给这个promise_type么
作者回复: 感谢你! using promise_type = promise<T> 是给primise<T>起了一个别名,这个别名是promise_type 等同于C++98中的 typedef promise<T> promise_type
2023-11-07归属地:上海1 - 常振华说实话,从这些代码来看,我认为比传统的异步编程更复杂了,而且是复杂太多了。 学到这里,我觉得concept是个好东西,相比原来的模板元编程,可读性友好了很多,其它的,就是把简单的事情复杂化。
作者回复: Modules其实是只有在所有的新代码都采用Modules后才能深刻感受的,但是C++需要引入非Modules代码,肯定就会使得问题反而复杂化了。 Coroutine中,C++标准只提供了“协议”,具体的实现需要我们根据自己的需要来做。但是如果在封装好一个完善的框架后是可以大幅度降低异步编程的复杂性的——就像我们可以在C++22 Coroutine的基础上实现一个完善的类似于libuv之类的异步I/O框架,应用开发者在框架的基础上开发应用就能降低复杂度了,而框架的开发者也能有足够的灵活性和自由性。因此我们需要从一个使用者的角度来看Coroutine,而不是框架底层的开发者,这样就不会觉得Coroutine会提升复杂性了。
2023-09-20归属地:广东 - !nullCountGenerator doCount() { for (int32_t i = 0; i < 3; ++i) { co_yield i; } } 这个返回值是在哪里return的呢?
作者回复: co_yield就是在暂停协程,并将值“return”回调用者。
2023-08-29归属地:北京 - sea520生成器在执行代码的时候,调用者也会在执行自己的代码吗,还是调用者暂停执行? 调用者是通过自己不断循环来获取生成器的返回值,还调用者怎么去做自己的事情,感觉不太能体现异步编程。异步编程是生成器有数据后通知调用者,而不是一直循环去检查是否数据完成吧
作者回复: 生成器在执行代码的时候,调用生成器的代码必然需要暂停等待被调用者返回结果。 但这里是同步还是异步是需要看你如何管理调用者和被调用者的执行。 比如,如果像我们这种简单的生成器,肯定就是同步的,但如果现在调用者和被调用者是在两个线程上,并且两者都是可被调度的任务,当调用者等待生成器时就可以通过调度将本线程的CPU让给其他需要CPU的任务,如果能有这种调度框架,那么从整体上来说就是异步的,因为这种情况主线程基本时不会阻塞的,永远会通过调度器分配任务,不会浪费等待生成器返回结果时的CPU时间。因此同步/异步模型还是需要从不同角度和不同的实现方式来看。协程为我们封装这种能力,实现相关的框架提供了一个技术上的基础。
2023-04-12归属地:浙江 - tang_ming_wu个人觉得,作为普通开发者,当前只需要了解无栈协程的基本原理即可。最后使用的,应该还是标准库需要提供的易用接口。
作者回复: 标准库对coroutine的支持预计还需要等待后续的标准(C++26或之后)。但是在这之前,C++已经从核心语言特性层面,支持了协程,如果要在工程中使用协程实现非嵌入式的高性能异步计算能力,已经是可能的了。 不过,就像正文所说的,目前这些接口约定还需要开发者实现。我在课程的配套代码中实现了协程的接口约定,可以参考其实现基于协程的高性能异步。 或者,等待 std::lazy 落地。
2023-03-14归属地:广东 - momo请教老师一个问题: 我们知道协程实现有 有栈协程和无栈协程,C++ 选用了无栈协程是基于什么考虑?
作者回复: C++选用了无栈协程主要是由以下考量: 1. 无栈协程具备更好的伸缩性和扩展性,相当于把对协程的高层接口语义定义的权力全部交给了开发者,因此可以允许开发者设计开发协程库,甚至基于无栈协程构建类似于有栈协程的高层接口。而C++26可能也会加入lazy,提供类似于有栈协程的封装,降低对原有代码的侵入性。 2. 与已有的其他基础设施,比如线程等完全正交设计,可以无缝集成。 3. 因为可以自定义挂起和恢复的实现,因此相比有栈协程可以更加高效。 4. 在有些团队或者公司可能因为各种原因会禁用异常,这种情况下自定义的无栈协程会有更好的性能。 总之,就是在日后的标准(比如C++26)中加入便于初学者使用的类似于有栈协程的接口,又允许专家开发者根据自己的需求去根据实际问题场景做深度的定制封装,这就是C++标准的一个设计原则。如果直接用了有栈协程,那就基本封死了开发者扩展定制的可能性,不符合C++的基本设计理念。
2023-02-19归属地:江苏 - peter请教老师几个问题: Q1:局部变量的声明周期是什么? Q2:用“exposition only”标识出来的ptr怎么跟编译器有关? 文中说“用“exposition only”标识出来的部分,就是 coroutine_handle 的内部存储内容,这部分只是为了说明标准做的示例,实际不同编译器可以根据自己的需求定义这里的实现”。 我的理解是:ptr应该是coder设置的具体内容,应该和编译器无关。 Q3:协程可以用来做长耗时的运算吗?
作者回复: Q1:局部变量的生命周期可以理解成这个变量在堆栈上的存在时间,超出生命周期时这个局部变量就会被销毁,如果是对象就会调用析构函数。 Q2:exposition only是为了解释编译器所提供的标准库实现可能是如何实现coroutine_handle的。coroutine_handle是C++标准库提供的标准类型,这个类型不需要coder自己实现,但是C++标准只定义了coroutine_handle的public部分的接口,内部如何实现完全由编译器包含的标准库具体实现决定,这就是为什么“编译器可以根据自己的需求定义这里的实现”。 Q3:协程完全可以用于长耗时运算,但是如果这样我们会利用线程或者进程通信、网络通信等机制将计算释放给其他的计算单元计算,这样当前线程还可以继续其他计算任务。协程只是提供了计算任务的手动控制切换的能力,和其他的并行并发计算技术是完全正交的。
2023-02-02归属地:北京