V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
kkhaike
V2EX  ›  C++

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

  •  
  •   kkhaike ·
    kkhaike · 2023-03-28 17:09:43 +08:00 · 3234 次点击
    这是一个创建于 630 天前的主题,其中的信息可能已经有所发展或是发生改变。

    项目

    起因

    公司一些老项目使用了 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. 这个思路有前景吗?如果有的话,我会投入一些时间到这个项目
    第 1 条附言  ·  2023-03-28 18:40:19 +08:00
    '在一个线程中开始启动协程' -> '在当前线程中开始启动协程'
    第 2 条附言  ·  2023-04-04 19:11:58 +08:00
    增加 sco::all 函数,依赖同时 co_await 多个协程,并且重构后又添加了大量注释,准备添加硬核实例(基于 libuv 以及回帖中提到的),和添加 README.md
    第 3 条附言  ·  2023-04-08 13:40:50 +08:00
    完成一个使用 libhv 和 redis++ 的实用例子 https://github.com/kkHAIKE/sco/blob/main/example/httpcache/main.cpp
    下一步实现 任意协程框架对接
    第 4 条附言  ·  2023-04-08 14:25:34 +08:00
    发现使用 [CppCoro]( https://github.com/lewissbaker/cppcoro) + single_consumer_event 配合也能做到这个效果。。。
    第 5 条附言  ·  2023-04-08 19:36:20 +08:00
    CppCoro 还是很难解决这个问题,不好处理根协程的引用和释放问题
    第 6 条附言  ·  2023-04-10 11:18:10 +08:00
    该项目已经 release 了第一个版本,让老项目使用协程 happy 起来吧
    23 条回复    2023-04-21 16:28:14 +08:00
    ysc3839
        1
    ysc3839  
       2023-03-28 17:18:07 +08:00 via Android
    你这个库引入了额外的线程,能兼容现有的库吗?我自己的方案是仿照 JavaScript 的 Promise
    https://gist.github.com/ysc3839/91dfa5b4f80caa05ea6d062476bbb66c
    kkhaike
        2
    kkhaike  
    OP
       2023-03-28 17:21:39 +08:00
    @ysc3839 并没有引入额外线程。。那个 test 里面有 async 做实验的。线程是靠原有的异步框架管理的
    kkhaike
        3
    kkhaike  
    OP
       2023-03-28 17:27:10 +08:00
    上面文案有误 '在一个线程中开始启动协程' -> '在当前线程中开始启动协程',才会造成上面的误解
    ysc3839
        4
    ysc3839  
       2023-03-28 18:19:23 +08:00 via Android
    @kkhaike 好吧,是我没看代码,看文字理解错了
    leonshaw
        5
    leonshaw  
       2023-03-28 18:59:25 +08:00
    这样只能把本来回调里的逻辑移到外面,但是有的场景回调参数生命周期只在 func 内部(这应该是无栈协程的硬伤)。 另外协程 resume 在异步函数内部的线程,如果是个第三方库提供的,可能影响它的线程管理。
    kkhaike
        6
    kkhaike  
    OP
       2023-03-28 19:09:05 +08:00
    @leonshaw
    1. 周期在内部的,可以考虑 move 出来,很少有不能 move 的吧。。
    2. 方向反了。。是异步线程 resume 协程,不会影响线程管理,实际上整个执行流程和你写回调地狱是一致的。。

    感觉这套逻辑比较抽象,有点难解释,但是能够最大限度保证无依赖无损接入协程
    leonshaw
        7
    leonshaw  
       2023-03-28 19:52:02 +08:00
    @kkhaike
    1. 考虑一个 visitor 函数,实现是持有锁的时候调用回调,然后释放锁,回调参数是某种 iterator 。对这个 iterator 的 move/copy 没有意义,因为一旦释放锁,访问就不是安全的。
    2. 没反,比如一个库内部有一个线程池,协程 resume 以后会阻塞这个线程。你可能认为本来回调就是运行在异步线程上的,但是两个 co_await 之间并不是只有原来回调的逻辑。
    ysc3839
        8
    ysc3839  
       2023-03-28 19:54:46 +08:00 via Android
    @leonshaw 协程 resume 就等价于调用回调函数,协程会阻塞这个线程的话,回调函数也会,并没有问题
    jdz
        9
    jdz  
       2023-03-28 19:57:37 +08:00 via Android
    存量库还是不能用吧,比如各种 client ,redis++ client mysql client blabla
    leonshaw
        10
    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
        11
    ysc3839  
       2023-03-28 20:14:09 +08:00 via Android
    @leonshaw 正常情况下 co_await 后面所有内容都属于回调函数内的
    kkhaike
        12
    kkhaike  
    OP
       2023-03-28 20:17:22 +08:00 via Android
    @leonshaw 没大懂你的点。。如果那个地方写了 cin 那么 相当于是 把 cin 写在回调里面,同样也是阻塞了回调函数啊
    kkhaike
        13
    kkhaike  
    OP
       2023-03-28 20:18:43 +08:00 via Android
    @jdz 只要满足上面说的 回调函数范式就能用
    kkhaike
        14
    kkhaike  
    OP
       2023-03-28 20:21:41 +08:00 via Android
    另外上面说不能 move 和 copy 的情况可以不走 co_await ,直接按普通函数调用也不会阻碍流程
    jdz
        15
    jdz  
       2023-03-28 21:01:44 +08:00
    @kkhaike 异步客户端一般都是在自己的线程中调用异步回调, 比如异步 redis 客户端, 那么在 callback 中唤醒挂起的协程?
    kkhaike
        16
    kkhaike  
    OP
       2023-03-28 21:15:57 +08:00 via Android
    @jdz 协程在当前线程调用客户端后马上挂起,在回调线程中继续恢复协程
    jdz
        17
    jdz  
       2023-03-28 21:17:45 +08:00
    @kkhaike 还没细看文档, 设计这样的协程是需要符合某些接口规范吧? 要不回调线程也不知道怎么唤醒挂起的协程
    kkhaike
        18
    kkhaike  
    OP
       2023-03-28 21:26:43 +08:00 via Android
    @jdz 文档就是告诉你如何实现规范
    jdz
        19
    jdz  
       2023-03-28 21:48:52 +08:00 via Android
    @kkhaike 我还是有个疑问,比如 redis client ,callback 是在 redis client 库自己起的线程中执行的,库自己的线程怎么知道执行完 callback 后去唤醒协程呢,我猜是编译器在 callback 代码块的后面加了唤醒的代码?
    jdz
        20
    jdz  
       2023-03-28 21:49:39 +08:00 via Android
    @kkhaike 我还是有个疑问,比如 redis client ,callback 是在 redis client 库自己起的线程中执行的,库自己的线程怎么知道执行完 callback 后去唤醒协程呢,我猜是编译器在 callback 代码块的后面加了唤醒的代码? 这么说的话 callback 也要符合文档规范?
    kkhaike
        21
    kkhaike  
    OP
       2023-03-28 22:34:59 +08:00 via Android
    @jdz 是我加的,协程接口主要是实现 co_await 这个关键字的细节,我在给异步函数的回调中加入了恢复协程的代码
    kkhaike
        22
    kkhaike  
    OP
       2023-04-10 11:18:18 +08:00
    该项目已经 release 了第一个版本,让老项目使用协程 happy 起来吧 😊
    weeei
        23
    weeei  
       2023-04-21 16:28:14 +08:00
    good ,mark
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5697 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 03:23 · PVG 11:23 · LAX 19:23 · JFK 22:23
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.