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

libfv:基于 C++20 的异步 HTTP 库

  •  1
     
  •   fawdlstty · 2022-04-23 20:52:46 +08:00 · 4799 次点击
    这是一个创建于 949 天前的主题,其中的信息可能已经有所发展或是发生改变。

    仓库地址:https://github.com/fawdlstty/libfv

    介绍一款船新 HTTP 库。C++的 HTTP 库很多,但基于 C++20 的异步网络 HTTP 库几乎没有。我没找到好用的,因此写了一个。在讲解这个库之前,我先说说为什么我们需要这样的库。

    C++ HTTP 库有两种主要的实现方式,第一种是同步 HTTP 网络访问,比如这样的代码:

    // 伪代码
    Response _r = HttpGet ("https://t.cn");
    std::cout << _t.text;
    

    这样的代码写起来很简单,但它存在一个问题:HTTP 网络访问比较耗时,可能需要几百毫秒,这么长时间,这个线程将阻塞在这里,比较消耗线程资源。假如遇到需要同时发起几十、几百个请求,将较大消耗系统资源。很显然,它不是一个较好的设计。

    第二种是回调通知,比如这样的代码:

    // 伪代码
    HttpGet ("https://t.cn", [] (Response _r) {
    	std::cout << _t.text;
    });
    

    这种方式解决了线程问题,也就是,几十、几百个请求可以同时发起,只需要极少量或者一个线程就行,HTTP 库内部实现了请求的内部管理,在收到请求的回复后,调用回调函数,从而实现请求的高效处理。但这种方式有个问题,假如我们需要根据请求结果内容转给下一个请求,这会带来一个回调地狱问题,比如这样的代码:

    // 伪代码
    HttpGet ("https://t.cn", [] (Response _r) {
        HttpGet (_t.text, [] (Response _r) {
            HttpGet (_t.text, [] (Response _r) {
                HttpGet (_t.text, [] (Response _r) {
                    HttpGet (_t.text, [] (Response _r) {
                        std::cout << _t.text;
                    });
                });
            });
        });
    });
    

    那么,有没更好的处理方式呢?有,通过 C++20 的 co_await 实现异步等待。下面给出 libfv 的发起请求的代码:

    fv::Response _r = co_await fv::Get ("https://t.cn");
    

    一方面它能获得回调方式的好处,也就是少量线程支撑同时大量的请求任务,同时它不会带来回调地狱问题。上面的代码通过 libfv 实现,代码可以这样写:

    fv::Response _r = co_await fv::Get ("https://t.cn");
    _r = co_await fv::Get (_r.text);
    _r = co_await fv::Get (_r.text);
    _r = co_await fv::Get (_r.text);
    _r = co_await fv::Get (_r.text);
    std::cout << _t.text;
    

    这儿特别说明一下。单 CPU 处理效率来说,C++20 的异步性能比回调要低,大概 10%左右,也就是假设理论上跑满网络 IO 带宽情况,回调需要 10%的 CPU ,那么使用 C++20 的异步需要 11%,这是 stackless 需要付出的代价。当然,在我看来这个特性完全可以忽略,毕竟 IO 密集型应用首先需要考虑的是跑满网络带宽,一般不太需要关注 CPU 使用率。

    libfv 使用方法见仓库:https://github.com/fawdlstty/libfv

    61 条回复    2022-04-27 10:03:35 +08:00
    kizunai
        1
    kizunai  
       2022-04-23 22:22:10 +08:00
    mark 一下,以后可能会用得到
    Calatrava
        2
    Calatrava  
       2022-04-23 22:25:02 +08:00
    连接池,dns 缓存这些都没有。实际项目里做不了什么事的。
    fawdlstty
        3
    fawdlstty  
    OP
       2022-04-23 22:33:03 +08:00
    @Calatrava 关于连接池,这个确实没有,后期会加入 http pipeline ,实现链接复用;第二个 dns 缓存,这个得自己处理。库这边已经支持了指定服务地址的功能,也就相当于手写一个 std::map 。当然,dns 缓存这块如果你能想到较好的解决方案,我加进去也行
    rophie123
        4
    rophie123  
       2022-04-23 22:34:53 +08:00
    c++11 才用上
    leimao
        5
    leimao  
       2022-04-23 22:36:01 +08:00 via iPhone
    不错,但看成了 lib“废物”,狗头
    fawdlstty
        6
    fawdlstty  
    OP
       2022-04-23 22:42:42 +08:00
    @leimao 网络库不都是 lib?v 么? libev 、libuv 、libhv 、etc.(狗头
    leimao
        7
    leimao  
       2022-04-23 22:43:23 +08:00 via iPhone
    @fawdlstty fv 看上去很像 fw ,我肯定是弹幕看多了
    ysc3839
        8
    ysc3839  
       2022-04-23 22:53:37 +08:00 via Android
    个人觉得非高性能场景的话,用线程池+同步 http 库会更简单
    enchilada2020
        9
    enchilada2020  
       2022-04-23 22:56:00 +08:00 via Android
    仿佛在看 JS…从回调地狱到 await😌
    Danswerme
        10
    Danswerme  
       2022-04-23 23:09:44 +08:00
    @enchilada2020 我也这么感觉😂,虽然不懂 c++,但是这几种实现方式和 js 的那一套几乎完全一样啊。
    fawdlstty
        11
    fawdlstty  
    OP
       2022-04-23 23:11:57 +08:00
    @ysc3839 非高性能场合确实同步更简单,相比可以少写个 co_await 关键字。这种场合主要就看,是否考虑软件未来并发量上去的情况
    fawdlstty
        12
    fawdlstty  
    OP
       2022-04-23 23:15:16 +08:00
    @enchilada2020 c 艹的 co_await 就是微软的提案,来源就是 c#的 await 语法。用法和 python 、js 等等语言都一样。c 艹因为历史原因( c 艹标准委员会巨坑,一个特性得吵好久都没法进入标准),进度比其他语言慢的多。只能说关于 stackless 这块,勉强跟上了时代
    fawdlstty
        13
    fawdlstty  
    OP
       2022-04-23 23:17:42 +08:00
    c 艹语法进步了,c 艹原本的库还没能更新呢,一堆库全同步或者回调的用法,标准出来等了两年多了都还没啥好用的库,这都 2202 年了
    fawdlstty
        14
    fawdlstty  
    OP
       2022-04-23 23:23:29 +08:00   ❤️ 1
    @Danswerme c 艹 20 是不是很简单,和 js 一样(狗头
    FrankHB
        15
    FrankHB  
       2022-04-24 04:14:44 +08:00
    @fawdlstty 硬塞半成品进去就是拖到下一版本擦屁股,结果升级火葬场。实现碎片化一地:VS 支持的特性 GCC 没有,GCC 有的 Clang 又没,这样朝三暮四几年实现才勉强能用,然后又赶上新的版本出来了。估计接下来几年现实一点就是 C++11 直接跳到 C++26 ,中间版本别看了。
    至于 async 这种说白了也是半成品,况且传染起来可一点不比 callback 含糊。js 用户都有不爽的:blog.logrocket.com/async-await-is-the-wrong-abstraction
    同样是半成品,怎么看都不如这种:
    open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0534r3.pdf
    其实都挺过气的,co_备胎到处都是。
    legacy.cs.indiana.edu/~sabry/papers/yield.pdf
    http://www.inf.puc-rio.br/~roberto/docs/MCC15-04.pdf
    ysc3839
        16
    ysc3839  
       2022-04-24 09:16:33 +08:00 via Android
    @fawdlstty 我指的是线程池配合 co_await ,这样侵入性更小,不需要占用一个线程来跑事件循环,尤其适合主线程跑 GUI 的程序。
    另外 C++的 coroutine 和 Python 的不一样,后者是和事件循环强绑定的,不能独立运行。C++的和 js 的类似,可以独立运行,可以代替回调函数。
    janus77
        17
    janus77  
       2022-04-24 09:44:20 +08:00
    rxcpp 有试过吗
    fawdlstty
        18
    fawdlstty  
    OP
       2022-04-24 10:10:55 +08:00
    @FrankHB 1 、co_await 确实是半成品,直接用确实存在你说的那个问题,但我不是直接用,我用的 boost.asio ,如果这个库也存在你说的问题,那说明你用错了
    2 、co_await 作为半成品的原因是,只提供了语法支持,需要自己造 awaitable 对象,c 艹 23 将在标准库引入 awaitable 对象,这代表老的 awaitable 也能继续用,因此升级也不会火葬场。即使库中的 awaitable 全体升级到 c 艹标准的 awaitable 对象,也能 ide 里全局字符串替换,几乎不会有啥问题
    3 、任何技术都不可能 100%让所有人满意。我做这个初衷也不是为了在所有场合都能使用,至少,c 艹 17 及之前版本不能用。我做这个库的目的是为了方便实现 co_await 方式去等待 http 响应
    fawdlstty
        19
    fawdlstty  
    OP
       2022-04-24 10:12:28 +08:00
    @ysc3839 想要实现 co_await 等待首先就必须得有一个线程池。我这儿是写死的一个线程池就一个线程。其实也可以多个
    fawdlstty
        20
    fawdlstty  
    OP
       2022-04-24 10:14:12 +08:00
    @janus77 没有。看了下,也和 http 没啥关系吧
    fawdlstty
        21
    fawdlstty  
    OP
       2022-04-24 10:21:11 +08:00
    @ysc3839 其实所有语言的 stackless 都是一样的,实现都是状态机+线程池管理异步任务,区别只有底层实现的代码不同、用起来给人感觉不同。对其他语言来说,我不是深度体验用户(因此对这个议题具有比较强的主观看法),不过你可以参考我以前写的一个库的源码,https://github.com/fawdlstty/SMLite ,里面有 js 和 python 的异步实现( c 艹的还没写进去)
    ysc3839
        22
    ysc3839  
       2022-04-24 10:23:56 +08:00
    @fawdlstty co_await 不一定要配合线程池用,可以当成回调函数来用,可以配合事件循环。但是事件循环需要跑在一个线程上,侵入性较大,复杂度也高。所以我认为要求不高的情况下用线程池+同步的 http 库会更简单,同时配合 coroutine 可以解决回调地狱的问题。
    forcecharlie
        23
    forcecharlie  
       2022-04-24 10:24:11 +08:00
    有没有一种可能,C++ 标准在网络这块不给力,大家都慢慢少用 C++ 开发网络程序了,比如可以使用 Golang/Rust 。

    C++ 网络标准就是一群人的零和博弈,互不相让,最后一拖再拖。

    免责声明:个人意见,并且本人在开源项目中大量使用 C++。
    ysc3839
        24
    ysc3839  
       2022-04-24 10:30:36 +08:00
    @fawdlstty 不同语言的 stackless coroutine 不完全一样,比如我前面提到的 Python ,不能实现 C++ 这种“由被 await 的对象控制恢复执行”,必须要一个事件循环,由事件循环来控制恢复执行。js 和 C++的有点类似,也可以由被 await 方恢复执行。C++是给被 await 方一个 coroutine handle ,“调用”这个 handle 恢复执行,而 js 是让被 await 方返回一个 Promise ,通过 Promise 这个中介来恢复执行。
    fawdlstty
        25
    fawdlstty  
    OP
       2022-04-24 10:36:09 +08:00
    @forcecharlie 截止 c 艹 20 前没任何标准,这也是 c 艹标准委员会效率低下的体现。但大家并没减少 c 艹开发网络程序,都开始尝试用三方库去做网络。go 和 c 艹不完全对标,没有可比性。rust 的话,相比 c 艹 03 及以前具有极大优势,但对于 c 艹 20 及以后版本,难说。
    fawdlstty
        26
    fawdlstty  
    OP
       2022-04-24 10:39:42 +08:00
    @ysc3839 所以这就是实现方式的不同咯。实际用户体验一致,比如 c 艹异步代码迁移到 python ,需要做的也是语法变为 python ,co_await 改为 await ,不会说换一种逻辑去实现
    tulongtou
        27
    tulongtou  
       2022-04-24 10:53:28 +08:00
    @fawdlstty 没有 libav 么?
    ysc3839
        28
    ysc3839  
       2022-04-24 10:55:20 +08:00
    @fawdlstty C++或 js 的 coroutine 不能直接迁移到 Python 。比如有个函数要求传递回调函数进去,js 可以用 Promise 转为在 async function 中 await ,C++也可以这么做。但是 Python 不行,Python 最多只能用 generator ,在回调函数中调用 next(generator)可以恢复执行。
    fawdlstty
        29
    fawdlstty  
    OP
       2022-04-24 11:08:54 +08:00
    @ysc3839 c 艹、c#、python 回调转异步做法是,创建原子信号量然后异步等待,同步回调里设置信号。异步等待任务收到信号后恢复执行
    ysc3839
        30
    ysc3839  
       2022-04-24 12:18:56 +08:00
    @fawdlstty 并不是这样,C++ co_await 一个 awaitable 对象时,会调用 awaitable 对象中的 await_suspend() 函数,并传递 coroutine handle ,当需要恢复执行时,只需要调用 coroutine handle 的 resume() 函数即可恢复执行。此处 coroutine handle 就类似一个回调函数。
    fawdlstty
        31
    fawdlstty  
    OP
       2022-04-24 13:38:17 +08:00
    @ysc3839 你说的是底下一层,这层的做法不兼容;我说的是顶上一层,这层的做法就兼容。让我选做法我会选择最简单通用的方式。
    hankai17
        32
    hankai17  
       2022-04-24 14:58:51 +08:00
    关注了 问两个性能问题
    1. c++20 的协程切换需要多久?
    2. 协程调度器效率问题 从一个协程 await 保存上下文 到 resume 恢复上下文 需要多久?
    fawdlstty
        33
    fawdlstty  
    OP
       2022-04-24 15:06:54 +08:00
    @hankai17 很难回答。使用不同品牌 cpu 、编译为不同指令集、不同编译器的不同优化等级,估计对这个都会有影响。你可以看看这个,对原理说的很详细。http://purecpp.org/detail?id=2288 。我个人看法,效率就是 O(1),因此忽略性能问题(狗头
    wanguorui123
        34
    wanguorui123  
       2022-04-24 16:39:53 +08:00
    C 艹 没有 C 井 的 await ?如何用?
    Calatrava
        35
    Calatrava  
       2022-04-24 16:40:54 +08:00
    @fawdlstty dns 信息如果需要用户传进去就不太好用了呀,用户要维护很复杂的数据。而且,谁来做 dns 解析呢?你好像也没有把每个请求访问的 IP 地址传给用户,用户还要再搞一套异步 dns+dns 缓存。C++20 的开发者不能只局限于写 demo 啊。
    fawdlstty
        36
    fawdlstty  
    OP
       2022-04-24 16:42:17 +08:00
    @wanguorui123 c 艹 20 有 co_await
    fawdlstty
        37
    fawdlstty  
    OP
       2022-04-24 16:44:41 +08:00
    @Calatrava 你说的有道理,不过你见过哪个流行的库有做 dns 缓存的嘛?这个需求非常个性化,只能用户自己处理。libcurl 、asio 、libev/uv/hv 等等不都没做。不做这个不代表局限于 demo ,而是给用户自由发挥流有余地
    Calatrava
        38
    Calatrava  
       2022-04-24 16:45:38 +08:00
    FrankHB
        39
    FrankHB  
       2022-04-24 16:54:24 +08:00
    @fawdlstty 我说 C++硬塞半成品进去不针对 co_xxx ,而是适用于这些年来各种已有的主要特性的迭代升级。
    (从 C++20 开始这坑特别集中。)
    比如 lambda ,C++20 调过 this 的 capture 还加了 deprecation ,于是一些 capture 不写全永远别想兼容 C++11 onwards ,要么#ifdef ,默认 capture 这功能约等于没有。
    再如 char8_t ,变更了类型直接搞得想要兼容的 u8 string literal 没法写。
    就算只用最新版本的嘛……有些可预见的东西是残的。比如 strcuctrual binding 还缺 nested match 。是不是会顺便在补全功能的同时进来一丢 breaking compatibility 的东西进来,到时候还真没底。
    这种不会一步到位倒是也挺符合微软特色的(都不限于 C 艹,e.g. v2ex/t/845526#r_11546877 ;硬点的话,WG21 浮躁的“敏捷”发半吊子版的歪风一部分就得归功于微软)。
    结果……这不就是等等党永不为奴么。(就算 C++98 太残太恶心了,C++11/14 大多数用户还是能凑数的。)

    C++23 的 co_xxx 补充主要是 std 内部的东西,你既然都能积极从头造库的轮子了,和这里的关系反而不大。我说的 co_xxxx 的半吊子,主要是指 xxxx 本身理论上就有的局限性,而不是 C++里 co_这样的具体语法设计。
    如果你看懂了我提到的一些文献就不应该容易有这个理解偏差。不过如果你只是用过“工业语言”特别是 async 类似物,因为糊 IO 库的主力确实也就是这些圈子在倒腾,那我倒是可以理解你这样想的原因(不过俺寻思 Lua 也不咋学术啊……)。

    至于 asio ,你更该关心的是为什么推了那么多年还没进去,半路杀出来的 executor 又难产了。(当然这主要还 WG21 的原因。)既然 chriskohlhoff 那么高效热脸怼冷屁股的作风都推不大动,就更别指望别人了。
    另外,基于 asio 的 http 库这题材实在不新鲜了。虽然我没怎么关心,比如 beast 这种,没在这里跟进么?
    考虑这点,想要用你的库,就得长个心眼关心作为作者你和这些更大众得项目的维护持续性的差距了,这也是你要推广时需要考虑的点(当然开源嘛,基调都是爱用用不用润,所以不关心这个也无妨)。虽然这根本上不是技术问题,很遗憾,大多数用户现实能体验到的差异就是在这里。
    fawdlstty
        40
    fawdlstty  
    OP
       2022-04-24 17:10:36 +08:00
    @FrankHB 1 、你上面说的是对的,所以 u8string 之类的难用或者不稳定的特性我都没用到,新特性里我只用到了 co_await
    2 、beast 不好用,我是想一行 co_await 就能发起请求,beast 只能说,挺规范的
    3 、作为个人开发者我没法保证任何情况都能积极维护。用的人少就不说了,用的人多了,像 duilib 那样,就算作者不维护其实也能开枝散叶
    fawdlstty
        41
    fawdlstty  
    OP
       2022-04-24 17:11:04 +08:00
    @Calatrava 好吧。这个我后面加上
    fawdlstty
        42
    fawdlstty  
    OP
       2022-04-24 17:16:42 +08:00
    @FrankHB 我用 c 艹 20 的东西不是因为我特别喜欢新特性,主要原因是我非常喜欢 c#的 await 语法,迫不及待想在 c 艹里用而已。毕竟对于 c 艹来说,能像 c#一样简单的开发异步程序是一件很有意思的事
    ysc3839
        43
    ysc3839  
       2022-04-24 17:26:46 +08:00
    @hankai17 C++20 的 coroutine 更像是回调函数,不一定要调度器。我写了一段对比 std::function 的代码,可以参考一下 https://godbolt.org/z/sd496fdxP
    fawdlstty
        44
    fawdlstty  
    OP
       2022-04-24 17:55:13 +08:00
    @ysc3839 这个看起来像是,因为没有需要等待的东西,所以被编译器优化成了普通回调
    ysc3839
        45
    ysc3839  
       2022-04-24 18:23:44 +08:00
    @fawdlstty 针对这个问题,我简单改了下,先把所有“回调函数”保存到 vector 里,最后再逐个恢复执行 https://godbolt.org/z/noKM31KMf
    Yain
        46
    Yain  
       2022-04-24 22:22:34 +08:00
    难得在互联网上看见 C++ 人。分享一下我的协程异步框架吧: https://zhuanlan.zhihu.com/p/504313175

    特点:面向系统调用,可以快速吸纳已有的上层库(包括 OP 这个 HTTP 库)

    ( C++ 新人求 Star ,不然连工作都没有 T.T ): https://github.com/Codesire-Deng/co_context
    fawdlstty
        47
    fawdlstty  
    OP
       2022-04-25 09:11:59 +08:00
    @ysc3839 噢,不对,我看错了。应该这样说:类似回调函数的实现,但因为多做了一些事,导致性能要差很多
    iqoo
        48
    iqoo  
       2022-04-25 11:38:11 +08:00
    支持一个,但 HTTP 这么复杂的协议造轮子太费精力了吧,而且 HTTP 一直在更新,比如目前大都用 brotli 解压缩、H/2 协议。
    fawdlstty
        49
    fawdlstty  
    OP
       2022-04-25 12:57:10 +08:00
    HTTP1.1 还好吧,简单的协议。HTTP2 、HTTP3 得找其他轮子了
    hez2010
        50
    hez2010  
       2022-04-25 13:16:15 +08:00
    async/await 这套原理上是非常高效的,并且是通用的异步方案,调度也并不依赖线程池。
    MSVC 团队的人之前在 CppCon 上展示了用 C++ 20 的 coroutine 做 CPU prefetch 来提升 CPU 缓存命中率,性能和你人工写的高度优化的状态机没有任何差距: https://isocpp.org/blog/2019/09/cppcon-2018-nano-coroutines-to-the-rescue-using-coroutines-ts-of-course-g
    这种层面的东西是 goroutine 、project loom 等 stackful coroutine 根本没法企及的,他们的作用只是减少 blocking ,而 async/awaiit stackless coroutine 出发点是设计出一套通用的异步方案。
    hez2010
        51
    hez2010  
       2022-04-25 13:29:45 +08:00
    OP 里的测试之所以比回调慢 10%,估计也是因为有部分代码没被编译器成功 inline 掉,这个需要钻一下写法或者等待后续编译器的改进。
    说“半成品”也是不妥当的,async/await 在语言层面上已经是完全体了,只不过 STL 里面没提供一个实现好的 Awaitable 罢了。并且在 Windows 上 WinRT API 里也有 `IAsyncAction` 和 `IAsyncOperation<T>`,都是按照 C++ 20 的 coroutine type trait 封装的 Awaitable ,因此如果你是做 Windows 开发( C++)的话,那也不需要等 STL 的 Awaitable 的,因为 Windows 的现代 API 本身就天然是异步的,并且已经提供了相关的实现。
    hez2010
        52
    hez2010  
       2022-04-25 13:33:21 +08:00
    顺带附一个 Windows 系统开发者写的 C++ coroutine 教程系列:
    https://devblogs.microsoft.com/oldnewthing/20210504-01/?p=105178
    fawdlstty
        53
    fawdlstty  
    OP
       2022-04-25 13:48:56 +08:00
    @hez2010 赞同你的观点,原理上非常高效,并且通用。不过,回调在汇编层面,寄存器或线程栈里存入参数,call 就行了; co_await 复杂很多,一方面涉及保存 /加载现场( pushad/popad ),另一方面还得有状态机等等。在具有真正需要等待的任务面前实际上没法做比较明显的优化,顶多也就是 llvm 的那一套,逻辑一句也少不了。c#在异步这一块往前跑了很多年了,基于 Task<>的优化方案是通过 ValueTask<>,实际原理是没有真正异步等待的优化为同步调用实现。它也没法优化真正需要异步等待的情况
    FrankHB
        54
    FrankHB  
       2022-04-25 20:56:38 +08:00   ❤️ 1
    @fawdlstty 我能理解 async/await 有一些能好用的地方,所以大约能理解你的动机。
    除此之外,如果你的目的是简化使用,照理来说应该赞扬,不过我不觉得能乐观。因为这方面不管是特性难产还是碎片化,很大程度来自 C++已有的设计,而频繁发版则在现实应用加速暴露了这些问题。

    你说 u8string 之类的难用的可以不用,如果就是库,确实不难做到。然而这里更多是核心语言特性的设计缺乏前瞻性和可组合性的问题,特别是 await 更改的本质上是相当底层的东西( control effects )却极少有人预料到缺乏一个靠谱的全局设计的风险,未来什么时候跳出来会咬你一口,还真不好说。
    只能到时候希望你能坚持初心。祝你好运。

    @hez2010 我不得不跑点题(虽然跟之前说的也有关)。
    async/await 在一些特定的场合是可用的方案,但距离通用差远了。
    光是之前提到过的一些用户抱怨的关于 async 的传染性问题,就是不把 async/await 改掉不再 async ,就不可能解决的(原因下面说)。
    另一方面,要是这个通用,这里按理根本就不用 stackful 的重量级玩意儿了(虽然这些东西能解决的问题比区区一个异步更广)。但现实呢?

    现实的异步编程的一个关键问题就是怎么用看上去表面同步的 direct style 去干掉显式 CPS 。其中的一个重要场景是,已有 direct style 的大量源代码,怎么在尽量少的改动下引入异步,允许异步的好处。
    直接 callback hell 显然不行。async/await 相对这种显然好,但还有明显的缺陷——要改的仍然太多了。具体来讲,async 和非 async 的部分原则上没法直接复用,这直接在逻辑上引入不必要的冗余。
    为什么 async 需要被传染,即便“异步”自身的逻辑上不必要?因为这种方案根本上要求改变 direct style 函数的返回类型。通过函数签名上附加标记,实现才能通过 syntactic transformation 的方式直接魔改代码生成(比如你说的状态机)糊上不同 style 之间的 semantic gap 。
    不过很遗憾,这就不是通用的需求:几乎所有现实异步编程的需求都不需要在乎返回类型和原先不同或者在乎生成的代码是否使用状态机(要是关心生成的代码,直接需求基本也就要开销小,尽量快);反倒是修改类型签名阻碍复用的作用暴露到对象语言用户面前,妥妥地绕不过(总不能逼用户自己糊状态机 8 )。所以这是一种具体实现造成的抽象泄露,断然不可能通用。

    进一步地,钦定类型签名的变化实质上是在代码添加静态信息(即便被关键字掩盖了)。而现代 C++的传统并不买 type richer = better 的账,所以强推 async 实际是不大自然的。
    例如,C++会强调 exception neutrality ,而和 Java 那种 checked exception 或者 Rust 的 Result 之类的机制(本质上都是庸俗的 union type )划清界限(即便有 expect ,也不会取代 exception ),因为不是所有情形都能对污染类型签名和 happy path 在现实中引起的修改的代价无所谓。
    反过来,在不愿意影响类型签名的问题上,要让代码更 C 艹-ish ,就意味着允许用户自行把多余非预期类型擦掉(即便有一些开销)。对对象类型,擦除是相对容易的( any ),但是对隐藏在 async 这样的关键字下的类型,这里的修改开销现实缺乏可行性。
    这意味着现实代码一旦引入 async 这样的特性,很可能在生命周期结束之前都没法甩掉,更加坐实了现实不可复用的缺陷。换句话说,要用了 async/await ,想要写语义上(而不只是表面的语法上)真正兼容同步和异步的代码,基本就不用想了。这又是一个不通用的例证。
    考虑到 dynamic exception specification 这样非签名侵入式的特性都是这般下场,我实在无法看好 async 真被滥用之后的景观。(虽然或许不过直接分裂用户群体成会接受 async 和不接受 async 的就好了……)

    最后,上面说的不可复用其实隐含了一点:不考虑就是否存在 async 实现 ad-hoc polymorphism 的扩展特性。
    实际上,对类型系统加以魔改,引入类似 Haskell 的 levity polymorphism ,理论上是可以直接解决复用问题的。
    但是,在此我旗帜鲜明地站在这种设计的对立面上,因为:
    它解决的问题和背后的复杂性不成比例实在过于离谱;
    C 艹的 value category 自带 subtyping ,直觉上就比 Haskell 的那几坨 kind 破烂难搞多了,瞎折腾就得寄,更别说做编译器的能理解了;
    会把 C 艹程序的不(可)可(读)理(性)喻(垃)程(圾)度连跳几级,推高到船新的阶段;
    async/await 说穿了也就是在以相当 syntactic 的方式实现区区那么一种 control effect ,甚至都没一般的 concurrency 排面,有什么脸占那么大核心语言特性演化资源?
    这种复杂度已经失控的边缘的设计再下去是不是甭想扩展了?

    最后的最后这点是我刻意要提到其它异步实现方式的直接理由;相对地,编程习惯的问题解决起来就小儿科多了。
    ysc3839
        55
    ysc3839  
       2022-04-25 21:09:30 +08:00 via Android
    @fawdlstty C++ 的 coroutine 并不复杂,主流的实现基本上是把 co_await 拆成 switch case 。简单举例的话,下面这段代码:
    ```
    promise async_func() {
    int i = 1;
    int cross = test1(i);
    co_await awaitable();
    test2(cross);
    }

    void main() {
    async_func();
    }
    ```
    编译后的结果类似于:
    ```
    struct coro_async_func {
    void run() {
    switch (current) {
    case 0:
    {
    int i = 1;
    cross = test1(i);

    current = 1;
    awaitable(this);
    break;
    }
    case 1:
    test2(cross);
    delete this;
    break;
    }
    }
    int current = 0;
    int cross;
    };

    void main() {
    (new coro_async_func)->run();
    }

    首先并不涉及“加载现场”,因为要跨越 co_await 的数据本来就保存在非易失的地方,更不涉及 pushad/popad 这种平台相关的操作,因为这套模式本来就是平台无关的。
    其次“状态机”并不会很影响性能,主流编译器选择使用状态机实现,而不是拆分成多个函数估计也是有评估过的。
    而 coroutine_handle.resume()也就是调用一下其中的 run()函数,和回调函数的性能一致。
    至于你后面说的优化那些我就不懂了,我没了解过别的语言的实现。但如果说 C++ coroutine 不如别的语言性能更好的话,那普通回调函数的性能也会不如。
    FrankHB
        56
    FrankHB  
       2022-04-25 21:37:01 +08:00
    @ysc3839 真说性能,其实状态机一般用途上还是比较快的,伺候好 cache 的话。现代 CPU 的 jmp 多少能投机,没以前那么坑了;反倒 call 还是那么麻烦。
    一般这类命令式语言中用的 IR 也对保留一整坨函数进行分析更不友好,扩大了偏好的影响。所以某种意义上,首选编译成状态机算是各家心照不宣的常识了。
    相比之下,stackful 的东西得搞 activation record ,即便有 CPU 的 stack engine 加成也是得在 key path 上多吃 cycle 的,极端性能就没法打了(虽然场景其实不大公平)。
    然而 C 艹现在这样的设计还是有损失性能的地方,就是一些东西扔 heap 这个投降得太早了,明摆着打不过 segmented stack+shared stack record 之类的。只不过现在的 C 艹也基本没法在这里走回头路。想在没搞定更基本的缺少怎么 reify the stack 的情况下还想折腾 ABI ,那基本就是找死。
    fawdlstty
        57
    fawdlstty  
    OP
       2022-04-25 22:59:20 +08:00
    @ysc3839 你是对的。我记错了。co_yield 涉及保存 /加载现场
    ysc3839
        58
    ysc3839  
       2022-04-26 09:07:15 +08:00 via Android
    @fawdlstty co_yield 就只是 co_await promise.yield_value(expr) 的语法糖,也不涉及加载现场
    https://en.cppreference.com/w/cpp/language/coroutines#co_yield
    fawdlstty
        59
    fawdlstty  
    OP
       2022-04-26 09:37:35 +08:00
    @ysc3839 好吧,我没认真看过,凭理解我感觉有 stackful 和 stackless 方案,然后默认它是 stackful 方案了
    fawdlstty
        60
    fawdlstty  
    OP
       2022-04-26 11:33:17 +08:00
    fawdlstty
        61
    fawdlstty  
    OP
       2022-04-27 10:03:35 +08:00
    @hez2010 找到一篇文章 https://zhuanlan.zhihu.com/p/395250667
    里面有句话“实际上,Future/Promise 并不适合性能要求很高的生产环境,比如游戏引擎任务框架等”
    co_await 也是 Future/Promise 吧。中间数据传递的类型都被擦除了(有状态,但编译器在优化时没法和 continue 方法一一对应),从 awaitable 对象外部看来也只知道返回类型是什么类型。使得这关键字从目前设计看来,没法优化
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2946 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 15:01 · PVG 23:01 · LAX 07:01 · JFK 10:01
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.