尝试写了一个最简单的 c++协程库,专治回调地狱,能对接任何全异步框架

2023-03-28 17:09:43 +08:00
 kkhaike

项目

起因

公司一些老项目使用了 brpc,写全异步的时候被回调地狱折磨,一想到现在都快 c++23 了,何不用协程解决问题,而现有的开源协程框架都要求从底层用起(很难与 brpc 结合)

思路

看完c++20 协程文档(感觉每一句都挺重要。。),有了用引用计数方案管理协程的想法

简单的说,就是当一个协程被 co_await 挂起后,由最晚运行的回调线程负责恢复,这样就不用从底层开始管理协程生命周期了

限制

所有包装使用的 异步函数 必须满足

  1. 函数无返回(void),并且回调函数也是:即类似 void func(int a, std::function<void(int)) cb)
  2. 函数正常运行,必定调用回调
  3. 函数不调用回调则必定抛出异常

样例

https://github.com/kkHAIKE/sco/blob/main/main.cpp

解释:

  1. some(1, 2).start_root_in_this_thread(); 在一个线程中开始启动协程
  2. co_await sco::call_with_callback(&test, a, b, sco::cb<void(int,int)>(c, d)); 包装 异步函数 test

输出

test return
some end
2,3
test return
some return
3,4

后记

  1. 各位大佬点个 star 吧
  2. 这个思路有前景吗?如果有的话,我会投入一些时间到这个项目
2928 次点击
所在节点    C++
23 条回复
ysc3839
2023-03-28 17:18:07 +08:00
你这个库引入了额外的线程,能兼容现有的库吗?我自己的方案是仿照 JavaScript 的 Promise
https://gist.github.com/ysc3839/91dfa5b4f80caa05ea6d062476bbb66c
kkhaike
2023-03-28 17:21:39 +08:00
@ysc3839 并没有引入额外线程。。那个 test 里面有 async 做实验的。线程是靠原有的异步框架管理的
kkhaike
2023-03-28 17:27:10 +08:00
上面文案有误 '在一个线程中开始启动协程' -> '在当前线程中开始启动协程',才会造成上面的误解
ysc3839
2023-03-28 18:19:23 +08:00
@kkhaike 好吧,是我没看代码,看文字理解错了
leonshaw
2023-03-28 18:59:25 +08:00
这样只能把本来回调里的逻辑移到外面,但是有的场景回调参数生命周期只在 func 内部(这应该是无栈协程的硬伤)。 另外协程 resume 在异步函数内部的线程,如果是个第三方库提供的,可能影响它的线程管理。
kkhaike
2023-03-28 19:09:05 +08:00
@leonshaw
1. 周期在内部的,可以考虑 move 出来,很少有不能 move 的吧。。
2. 方向反了。。是异步线程 resume 协程,不会影响线程管理,实际上整个执行流程和你写回调地狱是一致的。。

感觉这套逻辑比较抽象,有点难解释,但是能够最大限度保证无依赖无损接入协程
leonshaw
2023-03-28 19:52:02 +08:00
@kkhaike
1. 考虑一个 visitor 函数,实现是持有锁的时候调用回调,然后释放锁,回调参数是某种 iterator 。对这个 iterator 的 move/copy 没有意义,因为一旦释放锁,访问就不是安全的。
2. 没反,比如一个库内部有一个线程池,协程 resume 以后会阻塞这个线程。你可能认为本来回调就是运行在异步线程上的,但是两个 co_await 之间并不是只有原来回调的逻辑。
ysc3839
2023-03-28 19:54:46 +08:00
@leonshaw 协程 resume 就等价于调用回调函数,协程会阻塞这个线程的话,回调函数也会,并没有问题
jdz
2023-03-28 19:57:37 +08:00
存量库还是不能用吧,比如各种 client ,redis++ client mysql client blabla
leonshaw
2023-03-28 20:02:41 +08:00
@ysc3839
看 #7 ,协程并不只有回调,例如 op 的例子加几行:

co_await sco::call_with_callback(&test, a, b, sco::cb<void(int,int)>(c, d));
std::cout << c << ',' << d << std::endl;
std::cin >> a >> b;
co_await sco::call_with_callback(&test, a, b, sco::cb<void(int,int)>(c, d));
std::cout << c << ',' << d << std::endl;

正常情况下回调只有 cout ,但是这里 cin 也是阻塞在同一个线程的
ysc3839
2023-03-28 20:14:09 +08:00
@leonshaw 正常情况下 co_await 后面所有内容都属于回调函数内的
kkhaike
2023-03-28 20:17:22 +08:00
@leonshaw 没大懂你的点。。如果那个地方写了 cin 那么 相当于是 把 cin 写在回调里面,同样也是阻塞了回调函数啊
kkhaike
2023-03-28 20:18:43 +08:00
@jdz 只要满足上面说的 回调函数范式就能用
kkhaike
2023-03-28 20:21:41 +08:00
另外上面说不能 move 和 copy 的情况可以不走 co_await ,直接按普通函数调用也不会阻碍流程
jdz
2023-03-28 21:01:44 +08:00
@kkhaike 异步客户端一般都是在自己的线程中调用异步回调, 比如异步 redis 客户端, 那么在 callback 中唤醒挂起的协程?
kkhaike
2023-03-28 21:15:57 +08:00
@jdz 协程在当前线程调用客户端后马上挂起,在回调线程中继续恢复协程
jdz
2023-03-28 21:17:45 +08:00
@kkhaike 还没细看文档, 设计这样的协程是需要符合某些接口规范吧? 要不回调线程也不知道怎么唤醒挂起的协程
kkhaike
2023-03-28 21:26:43 +08:00
@jdz 文档就是告诉你如何实现规范
jdz
2023-03-28 21:48:52 +08:00
@kkhaike 我还是有个疑问,比如 redis client ,callback 是在 redis client 库自己起的线程中执行的,库自己的线程怎么知道执行完 callback 后去唤醒协程呢,我猜是编译器在 callback 代码块的后面加了唤醒的代码?
jdz
2023-03-28 21:49:39 +08:00
@kkhaike 我还是有个疑问,比如 redis client ,callback 是在 redis client 库自己起的线程中执行的,库自己的线程怎么知道执行完 callback 后去唤醒协程呢,我猜是编译器在 callback 代码块的后面加了唤醒的代码? 这么说的话 callback 也要符合文档规范?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/927907

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX