V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
hez2010
V2EX  ›  程序员

运行 100 万个异步并发任务需要多少内存

  •  1
     
  •   hez2010 · 28 天前 · 9008 次点击

    去年有一个 “How Much Memory Do You Need to Run 1 Million Concurrent Tasks?” 的文章测试了各种语言在运行 1 个、1 万、10 万、100 万个异步并发任务下需要多少内存,不过当时测试的版本都很旧,代码里也多多少少有各种槽点,不能反映最新的情况。

    这次把所有的语言版本都更新到最新,并且还加入了针对 GraalVM 、GraalVM native-image 和 .NET NativeAOT 的测试,然后修掉了之前被人指出代码中不对的地方,测了一个 2024 年版本的 “How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks?”。

    可以在这里看详细测试: https://hez2010.github.io/async-runtimes-benchmarks-2024 。测试环境和代码也在里面有说明。

    这里简单贴一下最终的测试结果:

    1 个任务,测各语言 runtime 本身的 footprint:

    1

    1 万个并发任务:

    10K

    10 万个并发任务:

    100K

    100 万个并发任务:

    1M

    Go 在最开始的时候内存占用很小,但是 Goroutine 的开销非常的大,随着并发任务的数量上升内存大量上涨,个人怀疑是 Go 的 GC 跟不上分配了,估计再接着增大并发数的话 Go 很可能会 OOM 。

    Rust 则是发挥稳定,从始至终都表现着非常好的占用水平。

    C# 的 NativeAOT 的表现则是直接把 Rust 比下去了,甚至随着并发数量的增大,到后期不做 NativeAOT 的 C# 内存占用都要比 Rust 更小了,可能是因为 tokio 和 async_std 在内存分配和调度这一块儿还有改进空间?

    Java 的 GraalVM 表现要比 OpenJDK 差很多,而 GraalVM 的 native-image 表现就要好不少。另外就是忽略 GraalVM 的成绩的话,从结果来看 Java 的 Virtual Thread 要远比 Goroutine 更轻量。

    第 1 条附言  ·  27 天前

    原文中给 Rust 追加了一个把 join_all 换成了循环依次等待的测试用例,tokio 的占用直接被砍半,成为占用最小的那一个。async_std 由于需要 poll 才能被调度因此无法这么做。

    tokio-for

    第 2 条附言  ·  23 天前

    接受了几个社区提交的代码改进,然后加上了执行时间和 CPU 占用结果,新的结果如下:

    https://hez2010.github.io/async-runtimes-benchmarks-2024/take2.html

    里面所有的结果图都可以通过点击具体的标签(Memory、CPU、Time)来隐藏某类数据,这样方便你针对一类项目来进行对比。

    简单放一下结果:

    minimal

    10K

    100K

    1M

    注意 Java 和 Python 在 1M 的时候已经无法在 10 秒内完成 benchmark 了,而其他语言都能正常在 10 秒内完成。

    192 条回复    2024-12-05 10:16:51 +08:00
    1  2  
    lesismal
        101
    lesismal  
       27 天前
    @lesismal @kneo #100 只是举例子哈
    lesismal
        102
    lesismal  
       27 天前
    @kneo

    > 感情我只要“同意”就是人云亦云?和你意见不一样就是“底层知识不了解”?您了解底层也没看您说出来“golang 的 goroutine 是预分配固定大小 2kb 的内存”这种话啊。

    前面说我不礼貌, 你不看为啥措辞这么火气, 因为你们阴阳在先
    我假定你不懂这个那个, 也是因为这么没意义的测试, 你在那阴阳而且很赞同, 但凡基础知识够用也会对这个测试产生质疑和更多的理性探讨, 就像其他很多楼层的兄弟提到的那样, 草率觉得这个测试没问题同意测试结论, 大概率就是基础知识不够. 如果我的这个假定是错的, 那你再看看我#96 替你惋惜的吧

    即使争吵, 我也真心祝你技术越来越进步, 这样可以避免大家以后更多争吵, 甚至会在同一个战线上同一个观点上去为别人厘清真相
    james122333
        103
    james122333  
       27 天前 via Android
    @kneo

    如果是这样 那应该是 java 纯用线程不用虚拟线程对 go 开 goroutine 比较 这样两者相比才是正确的 因为文内 go 的使用方式就是错的 使用对方法 go 也是可以大幅降低内存使用 而且使用上更简单 java 标准库写的一坨的东西在 go 内写一点就可以了 两者运作方式相近才有可比性 除非 go 没提供 否则这样对比有失公允 go 这样的作法明显就是把细节留给用户 更有弹性 用户知不知道是用户的事
    kneo
        104
    kneo  
       27 天前
    @lesismal

    > 即使争吵, 我也真心祝你技术越来越进步, 这样可以避免大家以后更多争吵

    谢谢了。不过避免争吵不是靠谁技术进步,而是平等和包容的讨论问题。技术进步是为了保护自己下次别吵输了。
    kneo
        105
    kneo  
       27 天前
    @james122333

    > 如果是这样 那应该是 java 纯用线程不用虚拟线程对 go 开 goroutine 比较 这样两者相比才是正确的

    Java 的虚拟线程就是对标 goroutine ,这两个都是 green thread 。我不太明白为什么不应该比较。

    > 因为文内 go 的使用方式就是错的 使用对方法 go 也是可以大幅降低内存使用

    关于文中 Go 的使用方式错误能否具体点?
    james122333
        106
    james122333  
       27 天前 via Android
    @kneo

    原因我在上面早就说了 这虚拟线程对标 go 应该是 goroutine+channel 你只用 goroutine 怎么可能达到一样效果 协程是微线程 没说一定得要负责负载小 文内作法就是有多少就是分散 cpu 资源并且增加内存使用
    standchan
        107
    standchan  
       27 天前
    @povsister #98 哈哈哈哈是的,我也看呆了。
    james122333
        108
    james122333  
       27 天前 via Android
    @kneo

    如果按文内 go 这种写法套用在其它语言使用同样运作机制都是爆炸的 内存绝少不了的 真要对比应该是 java 用实体线程不开队列一样使劲开
    kneo
        109
    kneo  
       27 天前
    @james122333

    > 虚拟线程是真实线程内用队列处理任务 其它的语言用事件驱动也是差不多道理 对标 golang 应该是 goroutine+channel

    goroutine 也是在原生线程里 run 的啊……
    james122333
        110
    james122333  
       27 天前 via Android
    @kneo

    但它并没有队列等机制确保负载不爆阿 等于 go 这部份你要自己做
    james122333
        111
    james122333  
       27 天前 via Android
    @kneo

    文内 go 这样做法就雷同更迷你的 java 实体线程狂开而已 开更多但效果一样雷同 虚拟线程还有其它语言实作的还多做其它事情
    kneo
        112
    kneo  
       27 天前
    @james122333 抱歉,没能理解你说的 golang“没有队列等机制确保负载不爆阿”。

    大家都是 green thread ,都需要去分配资源去占用原生线程。

    你的说法听起来是在表达 golang 的调度机制不够优秀。但你说因此要 Java 换原生线程来比好像还差点说服力。
    lesismal
        113
    lesismal  
       27 天前
    @james122333 @kneo

    继续上点货, golang timer+简单的 goroutine pool:
    https://gist.github.com/lesismal/aaf767b03f669908b1d5aae61a135a1c

    goroutine pool 就是用 chan 的简单实现
    timer 的是直接用的 B 站毛剑的这个单协程, 稍微改了下 timer 到时触发后用上面的 goroutine pool 去执行 func:
    https://github.com/Terry-Mao/goim/blob/master/pkg/time/timer.go

    具体需要多少 size 的 goroutine 自己设置, 10000 的并发度已经足够满足绝大部分需要了

    m3 macos 简单跑了下:
    1M task, goroutine pool size 10000, chan queue 1000, cost 115M
    1M task, goroutine pool size 1000, chan queue size 1000, cost 95M

    如果再改改时间轮之类的优化下海量 timer 的每个 itemsize, 还能再省些内存, 但是这个版本已经足够了
    lesismal
        114
    lesismal  
       27 天前
    @kneo 其实吵吵挺好的, 不打不相识, 技术这种, 只要理性吵, 越吵道理越清晰, 而且很多技术路线之争, 即使是在老爷子们或者不同社区之间, 也是互相很多争吵, 争吵是因为没有一个完美的方案, 吵的多了, 改进多了, 就越来越好了
    吵过了就都翻篇过去了, 以后咱也多多交流
    CRVV
        115
    CRVV  
       27 天前
    goroutine 从来都不是协程。

    在 go < 1.14 的版本里面,goroutine 的实现( implementation )确实是协程,但是它没有协程的接口( interface )。
    因为实现一样,大家喜欢叫它协程,这也没什么错。
    https://go.dev/doc/faq#goroutines
    这里有明确的解释,把协程做了一点修改,起了一个新名字 goroutine 。当时肯定更不能叫 thread 因为不是抢占式调度的。所谓 interface ,那就是用来以后改 implementation 的,所以专门搞了一个新名字,这事他们团队做得很明确。

    然后 1.14 把调度器改成了抢占式的。
    https://github.com/golang/go/issues/10958
    那这东西就完全不是协程了。

    中英文的内容里面都有很多人习惯说协程、coroutine ,你非要说这是你的习惯要留着它,那就留着吧也没什么关系。
    不过错的就是错的,别理直气壮地用错误词汇。
    james122333
        116
    james122333  
       27 天前 via Android
    @kneo

    go 就是使劲狂分资源而已 你要自己分配资源 这部分语言没有义务一定要做
    james122333
        117
    james122333  
       27 天前 via Android
    @kneo

    自己分配或靠第三方库帮你分配 有好处也有坏处
    kneo
        118
    kneo  
       27 天前
    @james122333 额,好吧,我还是维持我的观点。
    sofukwird
        119
    sofukwird  
       27 天前 via Android
    素养差一大节,你也能聊的下去,有点闲哦
    james122333
        120
    james122333  
       27 天前 via Android
    @CRVV

    现在 go 的运作确实如同一般线程 但我觉得说协程也没什么关係
    james122333
        121
    james122333  
       27 天前 via Android
    @kneo

    我还是那句 运作方式相近的测试才有可比性 c#类的只是官方有帮你做好而已
    kneo
        122
    kneo  
       27 天前
    @lesismal 我完全理解 goroutine pool 在生产环境里的作用。这东西 Java 用了几十年了,什么好说的。

    但是如果 goroutine 的成本真的便宜到几乎免费,我们大多数时候是不需要因为“性能问题”而使用 pool 的。使用 pool 的主要用例应该是间接限制 goroutine 所对接的外部资源。

    如果还是因为 goroutine 本身的性能原因而使用 pool ,我认为就语言实现层面还是差点意思。

    回到这个帖子所引用的原文,我认为它至少从一个方面回答了到底在哪还差了点。
    lesismal
        123
    lesismal  
       27 天前
    @CRVV

    > 不过错的就是错的,别理直气壮地用错误词汇。

    我想问下, 如果想用中文, 用什么词翻译 goroutine?
    约定俗称的事情你也是一点不提啊? 我十几年前就写 lua 用 coroutine, 刚开始用 go 也是看了一些人讨论的定义, 但是照样, 大家中文叫法就是协程, 因为即使讨论了说了 goroutine 不是协程, 也没人给它个正确的中文翻译并且流行起来, 所以大家继续这样叫.
    然后你就这么断言觉得别人就是不知道 goroutine 不是协程定义的那个协程并且错了还理直气壮是吗?

    这么自信吗? 普通话里"那可不是嘛", 这里用"不是"但意思是"是", 要不要也去给大众纠正批判下, 让大家平时说话不要这么讲错就错至少不要这么大声面的看上去犯了错还太过理直气壮?
    这种谬误和约定俗成的事情多了去了, 好几个人跑这卖弄贬低还不敢直接 at 人, 显得你们能是吧?
    james122333
        124
    james122333  
       27 天前 via Android
    @kneo

    手动派就是要自己来 不然整天因为别人实作的踩坑很烦 我比较喜欢 go 这样 不要坑我最好
    kneo
        125
    kneo  
       27 天前
    @james122333 我不认为不相似就没有可比性。性能测试的重点之一就是从一个方面提供不同策略不同实现之间的差异给人做参考。
    james122333
        126
    james122333  
       27 天前 via Android
    @kneo

    那这种测试结果本身基准就不平 得出的结论也就看看就好
    Kauruus
        127
    Kauruus  
       27 天前
    @james122333 感觉 java 的 ThreadPoolExecutor 才是你说的 goroutine + channel ,一个线程池 + 队列执行任务。

    Thread.startVirtualThread 和 go 一样是“使劲狂分资源”,然后给调度器(里面自然是有个队列的)调度,virtual thread/goroutine 遇到阻塞的时候把执行权给会调度器( Go 也有抢占调度,不知道 virtual thread 有没有),让它调度下一个 virtual thread/goroutine 。
    CRVV
        128
    CRVV  
       27 天前
    @lesismal

    你可以说线程,或者说用户态线程。
    技术词汇和 “那可不是嘛” 是两码事,就像你不能把硬盘叫内存,固态硬盘也不行,焊在主板上的也不行。

    但你说的最后一段就是纯扯蛋,不要论断人。
    james122333
        129
    james122333  
       27 天前 via Android
    @Kauruus

    队列才是本体 线程池就是队列 不管如何只要最终效果雷同就是差不多的
    kneo
        130
    kneo  
       27 天前
    @james122333

    > 那这种测试结果本身基准就不平 得出的结论也就看看就好

    额,怎么说呢……测试结果,看的就是某个语言使用某种实现能得到什么样的性能。我们只要客观的理解数字就可以了。

    只有带入过多感情,认为有些数值高的赢了,某些数值低的好像输了,才会纠结“公平”。
    lesismal
        131
    lesismal  
       27 天前
    @kneo #122

    > 如果还是因为 goroutine 本身的性能原因而使用 pool ,我认为就语言实现层面还是差点意思。

    是单个 runtime 的阈值. 我日常 4c8g vm 跑个压测, 10w 协程这种级别, 调度/内存/gc 都没什么压力的. 但是 50w 以上, 压力就大了起来, 1M 级别的压力就更大了, 耗费非常高的硬件仍然稳定性艰难.
    标准库是满足了绝大多数人的需求, 因为没这么大量, 足够好的性能, 但少量业务还是需要海量的, 能稳定高性能低消耗就能省很多硬件成本.

    > 回到这个帖子所引用的原文,我认为它至少从一个方面回答了到底在哪还差了点。

    不同的业务 golang 有不同的应对方案, 我前面也说过了, blog 作者用这种方式测这个并发任务属于错误用法, 而其他场景, 也会有对应的优化 goroutine 数量的方案.

    但有些场景确实目前还没有海量 goroutine 的优化方案, 例如 HTTP2.0 QUIC/HTTP3.0, 暂时还是一个连接至少一个协程的方案, 因为目前的 2.0 3.0 实现都还是基于标准库的 net.Conn 都还是阻塞的 IO 接口.
    我想把 nbio 做更多支持这些, 但是工程量太大了, 年纪也大了体力跟不上, 而且是业余时间为爱发电不是全职搞, 个人的精力消耗实在承受不起.
    我希望以后能有人继续把 HTTP2.0/3.0 以及更多协议也搞成类似 nbio 的这种方案, 然后就不用再担忧这些瓶颈了
    Kauruus
        132
    Kauruus  
       27 天前
    @james122333

    virtual thread 和 goroutine 是一个可以被调度的实体,或者说是用户态线程,有自己的栈。

    channel ,BlockingQueue 这些是消息通讯机制。

    然后调度器都是队列,把用户态线程调度到物理线程。

    所以我没明白你说的 “这虚拟线程对标 go 应该是 goroutine+channel”。
    james122333
        133
    james122333  
       27 天前 via Android
    @kneo

    如果这样比那就只是技术粗略比较 语言不同特性不同 怎么发挥该语言优势特点也都不同 怎么发挥还跟人有关 作者这样搞不是有目的就是真不知
    james122333
        134
    james122333  
       27 天前 via Android
    @Kauruus

    你是说 goroutine 就已经有调度了吗? 测试来看没有 它更像是单纯的线程 配合 channel 调度才能对标虚拟线程或线程池
    james122333
        135
    james122333  
       27 天前 via Android
    @Kauruus

    channel blockingqueue 效果都是队列的 再多几个队列只是细节调控
    kneo
        136
    kneo  
       27 天前
    @james122333

    > 作者这样搞不是有目的就是真不知

    作者就是随手撸几段代码测一下,我看到的是单纯的程序员的快乐。不要阴谋论啦。
    lesismal
        137
    lesismal  
       27 天前
    @CRVV #128

    你真逗, 社区里大家都叫协程, 你为了严谨, 非要叫"线程"或者"用户态线程".

    而且, 你让我用 "线程或者说用户态线程" 就更扯了:
    1. 线程比协程更具有辨识度, 通常是操作系统内核进行调度的, 比如 CSAPP 里这样讲:
    https://cs50mu.github.io/post/2016/08/29/csapp-concurrent-programming/

    Threads. Threads are logical flows that run in the context of a single process and are scheduled by the kernel. You can think of threads as a hybrid of the other two approaches, scheduled by the kernel like process flows, and sharing the same virtual address space like I/O multiplexing flows.

    2. 用户态线程通常是对应内核线程, 而一个 golang 进程首先是进程自己有一组(1-N 个)用户态线程, goroutine 是基于用户态线程之上的应用层语言 runtime 自己进行调度的, 最大的区别, goroutine 可不是由内核调度的

    你要是说 goroutine 是"类似线程的东西"这种模糊的定义还凑合, 但是直接把 goroutine 叫做线程或者用户态线程, 你可以做个问卷调查看看, 是我们把它叫做协程容易让人懵逼还是你的叫法更容易让人懵逼.

    > 技术词汇和 “那可不是嘛” 是两码事

    方言, 约定俗成这些事你如果不认同, 那咱们不用讨论这个了, 但是也请尽量弄清楚你自己的技术词汇是否真的准确, 比如 goroutine 线程.


    > 但你说的最后一段就是纯扯蛋,不要论断人。

    如果不想自己被他人断, 就不要自己先乱断他人.
    Kauruus
        138
    Kauruus  
       27 天前
    @james122333 Go 运行时不是会调度 goroutine 吗?

    > 它更像是单纯的线程 配合 channel 调度才能对标虚拟线程或线程池

    你不用 channel 它也会被运行时调度到物理线程上执行呀,运行时内部也有队列呀。
    james122333
        139
    james122333  
       27 天前 via Android
    @kneo

    他也许很快乐 但其它人不一定 阴谋论不是豪无根据就产生的
    CRVV
        140
    CRVV  
       27 天前
    @lesismal

    你就是个傻逼,block 了,也别回了
    lesismal
        141
    lesismal  
       27 天前
    @CRVV 刚又搜了下, 你说的"用户态线程"可能是指"纤程"吧? 对不起, 这玩意好像是 Windows 的, Windows 开发相关的知识我确实是不熟悉, 我说的主要是*nix 的
    james122333
        142
    james122333  
       27 天前 via Android
    @Kauruus

    我不知道它怎么实现 但队列长度过大细微调度型同虚设 就测试来看 没有看出与无队列相差多大
    james122333
        143
    james122333  
       27 天前 via Android
    @Kauruus

    所以我推测它应该是无队列 因为效果与一般线程是差不多的 都有消秏过多资源问题
    lesismal
        144
    lesismal  
       27 天前
    @CRVV #140

    自己在那不 at 别人偷塔断言别人不懂说别人错了还理直气壮, 然后老夫硬刚你, 技术和逻辑都说不过就飙脏话是吗?
    既要又要, 又 x 又 x 的, 当别人眼瞎好欺负呢?

    谢谢你 Block 我, 免的以后再来浪费我时间
    Kauruus
        145
    Kauruus  
       27 天前   ❤️ 1
    @lesismal

    > 而一个 golang 进程首先是进程自己有一组(1-N 个)用户态线程, goroutine 是基于用户态线程之上的应用层语言 runtime 自己进行调度的, 最大的区别, goroutine 可不是由内核调度的。

    “这组线程”就是内核线程,有对应内核调度实体,Goroutine 才是“用户态线程”。

    这是非常普遍的 M:N 调度模型。M 个 Goroutine/Green thread/virtual thread/ 调度到 N 个内核调度实体上。

    当然 Go 为了方便迁移 Goroutine ,还有个 P 的概念,不影响 M:N 的关系。

    按照你的说法,就变成 M:N:O 三层了。
    linshenqi
        146
    linshenqi  
       27 天前
    😺
    ykrank
        147
    ykrank  
       27 天前
    @kneo 啥呀,人家是阴阳怪气你技术不够才会和他吵呢,等你技术进步到和它“一个层次”了
    kneo
        148
    kneo  
       27 天前
    @ykrank 哈哈,无所谓了,我心里有数。
    lesismal
        149
    lesismal  
       27 天前   ❤️ 1
    @Kauruus #145

    > “这组线程”就是内核线程,有对应内核调度实体,Goroutine 才是“用户态线程”。

    在对线程的细分定义上讲, 用户线程你说的对, @CRVV 说的也对.
    我之前没有深究过用户线程这个细分概念, 把它理解成非内核创建的线程了, 是我的错.

    不论严格定义如何, 把 goroutine 叫成协程已经是大家的惯用叫法, 日常讨论说线程用于代表严格意义的内核线程多些, 除非涉及细分定义, 否则也不会去区分用户线程还是内核线程.
    但是如果把 goroutine 叫成线程, 是更让人混淆的, 把 goroutine 叫成用户线程是严格定义上正确但在日常交流上更多也是会带来麻烦
    各种细分的严格定义, 例如说线程的时候不叫线程而是叫轻量进程, 也是给非学术交流带来沟通障碍, 约定俗成的叫法可能会更适合日常讨论.

    > 按照你的说法,就变成 M:N:O 三层了。

    我只是想表达: 线程 -> go runtime -> goroutine, 没有想表达是几层, 具体到 goroutine 的调度就是 go runtime 的 GMP,

    而且几层也得看怎么划分:
    如果按照调度实体分类, 那就是两层: 线程被内核调度, goroutine 被 runtime GMP 调度
    如果是按这些抽象角色分类, M/P/G 也可以说是三层
    lesismal
        150
    lesismal  
       27 天前
    @ykrank

    > 等你技术进步到和它“一个层次”了

    你这个"它"很会用啊, 你技术强的话可以输出技术观点, 技术的观点一点没有, 有话都不敢直接说, 阴阳最有一套是吧?
    你咋不上天呢?
    lesismal
        151
    lesismal  
       27 天前
    @Kauruus
    @CRVV

    补充一点, 工作久了也不是钻研学术的, 很多概念定义也早忘光了, 我更多的是专注于实践, 所以有说的不对的地方是我不懂, 说错了我就认, 不会赖账的

    另外, 就像我前面说的没必要咬文嚼字, 聊具体的代码问题, 不需要纠结严格定义的, 咱就少点学术氛围, 免得搞半天也搞不出个成果来
    charles0
        152
    charles0  
       27 天前
    @lesismal
    goroutine 按定义不是协程,Go 语言官方的文档也从未直接称其为协程
    你可以直接叫它 goroutine ,不用翻译了,我相信 Go 语言设计的时候选择发明这个词是有原因的
    lesismal
        153
    lesismal  
       27 天前
    @lesismal #151

    甚至, golang 的 GMP, 我都忘记了, 要说 10 年 8 年 5 年甚至 3 年前, 应该都还记得, 但是现在都不记得了, 刚才回复 @Kauruus 也是临时搜了下 GMP 才又知道了个大概.
    而且这几年自己可见的速度在记忆力下降, 每次看到 golang 面试题的帖子, 很多也是不会, 只剩下一些实践的套路经验了, 上年纪了也没体力重新读这些书了, 不是不想, 而是真的力不从心了
    幸好工作上也不太需要这些概念了

    华山派剑宗风清扬我做不到, 但是像他一样的风格还是适合工程实践的, 什么这个气那个气的内力之类的, 先能把工程搞定并且搞好才是好
    lesismal
        154
    lesismal  
       27 天前
    @charles0 #152 叫 goroutine 没毛病, 但是打字中文的时候协程比 goroutine 快, 而且很多人约定俗称都这么叫了, 日常讨论, 何必搞得学术氛围甚至法庭审判那样? 你们好几位出来说这个, 我反向建议下你们在实际生活中要灵活, 否则方言俗语一切约定俗成的谬误的词汇就都不能说出口了, 这样的活法, 会很累而且并不会对交流效率带来提升, 反倒会因为这种死板给更多遵循约定俗成的人带来麻烦, 让大家交流效率更低
    sagaxu
        155
    sagaxu  
       27 天前   ❤️ 3
    Goroutine / Coroutine / Virtual Thread / Async-Await ... 名词一大堆,大家的共同点,都是提供比 OS thread 更轻量的并发执行上下文管理。提供这个设施的主要原因,无非是为了解决写复杂回调代码时的心智负担。

    内存占用差异,本质上就是这些实现在 stack 实现上的选择,大致有 2 类,
    stackful ,Goroutine 这种跑上来就分配 2KB/4KB 的栈,1M 并发时内存就要 2G/4G
    stackless ,async-await 方案大都如此,因为无栈,内存占用往往有很大优势


    把这两种方案放在一起 PK 内存占用,stackful 天然吃亏。Java virtual thread 本质上也是 stackful ,只不过很取巧的使用了可变尺寸栈,JVM 可以动态调整栈大小,不需要一开始就分配的很大。

    stackful 和 stackless 都是上个世纪的理论了,这些语言的设计者们不可能对此不精通。那为何没有一边倒的选择某种方案?显然是两种方案各有利弊。我不谈底层实现差异(主要是不懂),也不谈哪个更强(于我都够用),仅从使用者的角度谈一下感受。

    一,函数颜色问题。很多协程的实现,会把所有 IO 函数分成两类,一类会阻塞线程,另一类会挂起让出线程,两者之间往往不能互相调用,Kotlin 的 suspend 关键字就是给函数打标记用的,有些语言用 async 来标记也是一样的。使用这类语言的时候,我们需要清楚的了解调用的方法是不是能用于协程,使用第三方库甚至标准库的时候,也要小心翼翼,一旦弄错后果非常严重。

    二,是否抢占式调度。非抢占式调度,需要使用者自己主动交出资源,很多实现提供了 yield 方法/关键字,避免大循环独占线程太久饿死其它协程。

    Virtual thread(JVM)和 Goroutine 很好的解决了这两个问题,在集成第三方库的时候,基本上(当前版本 JVM 不能 100%做到)不用考虑会不会阻塞线程的问题,牺牲点内存提高开发效率,是好事还是坏事,没有定论,这要看具体的场景。

    理想中的协程应该是这样的,
    1. 足够轻量,fire and forget ,不需要 pool ,即便 pool 化了收益也很小
    2. 函数不分类,不存在不能用于协程上下文的函数
    3. 抢占式调度

    但这 3 点根本无法同时实现,Go 和 JVM 都选择了 2 和 3 ,Go 在 2 上面做的最好,毕竟你想写出点阻塞 OS thread 的代码还要动点脑筋。JVM 在 1 上优化的比 Go 好,Java 官方文档敢写“Represent Every Concurrent Task as a Virtual Thread; Never Pool Virtual Threads”。

    题外话,有了很轻的协程,就可以肆无忌惮的开了吗?并不是,DB 扛不住啊,所以像 Kotlin 这种 1G 内存能开 2M 个协程的语言,也特地提供 limitedParallelism 控制协程的并发度。这是调度层面做的,不一定要用协程池的方式来做。
    dogfeet
        156
    dogfeet  
       26 天前   ❤️ 1
    不是,这测的有啥问题吗?

    要测 100 万异步并发任务内存占用,可不就是构造个 100 万异步任务同时运行的场景吗?
    你可以说知道 100 万异步并发任务内存的占用了的指导意义有限,但你不能说用 `sleep` 去构造这个场景有问题吧。

    看的头疼,感觉没那么难理解啊。

    还有就是,虚拟线程也是 `stackful` 的,咋就不公平了呢?

    怎么没人替 java 喊喊冤啊。
    trzzzz
        157
    trzzzz  
       26 天前
    @grzhan 我之前看到很多框架都这样做,当时没太明白这样做的目的。这样做有啥好处可以简单说下嘛
    james122333
        158
    james122333  
       26 天前 via Android
    @dogfeet

    内文强调的是内存占用 以 go 范例这种用法内存不可能不爆的 所以称不公平合理
    很多人讲 sleep 不好的意思应该在通常不会有执行 10 秒以上的任务
    grzhan
        159
    grzhan  
       26 天前
    @trzzzz 是指定时器的 duration 会加上 10% 抖动的做法吗?
    mightybruce
        160
    mightybruce  
       26 天前
    我就几句话说, 先看看 rust, node.js 到底会不会每次在 async/await 中创建协程,这种 sleep 首先都没有什么资源消耗, 相关实现基于调度器或基于计时器的完全可以。
    node.js 直接通过 event loop 和 timer 来调度就行。
    其他讨论很多人已经提到了。
    CloveAndCurrant
        161
    CloveAndCurrant  
       24 天前
    @kneo 你是真能倒打一耙,“我认为”的是你,嘴硬的是你,抠字眼守旧抱着 wiki 当真理的也是你,然后你把这些臭毛病全都推给别人,自己一身毛硬说别人是妖怪,你是真滴像现在到处碰瓷的老 B 登🤣🤣🤣
    kneo
        162
    kneo  
       24 天前 via Android
    @CloveAndCurrant 诶呦,好几天了,咋还是那套车轱辘话啊。我以为你学了啥新东西来怼我呢。也是,您这辈子就这样了,连个术语都改不过来,哪还能学会新东西啊。昨天吃几碗饭没忘就不错了。

    不过你总碰瓷 wiki 干啥啊?你脸不要啦?你连个术语都搞错,还代表“工程领域”否定 wiki ,好在没人把你当回事。

    你觉得 wiki 是错的,但是你不说你觉得,那你就是对的了?你可真是个小天才哈哈。不知道你家长给你配手表了吗?

    你和 wiki 二选一,真不知道整个地球上除了你 MA 还有谁会选你。[捂嘴笑]
    CloveAndCurrant
        163
    CloveAndCurrant  
       24 天前
    @kneo 我能像你这个老古董一样不休息,就靠着这口抱着 Wiki 抠字眼抬杠活着😂😂😂,没水平的老登,早早滚蛋别挡路不好吗🤡🤡🤡
    kneo
        164
    kneo  
       24 天前 via Android
    @CloveAndCurrant 你也不会别的啊。上来就输出一个 goroutine 是协程然后还砸脚了,然后说别人“抠字眼”哈哈。然后除了吹牛,又“工程领域”又“wiki 不是真理”的,好像你亲妈给你起名叫真理似是。

    信不信让你爸来他都会选 wiki:宁要 wiki 不要逆子。
    CloveAndCurrant
        165
    CloveAndCurrant  
       24 天前
    @kneo 你能不能不要丢人现眼?😅😅😅,Wiki 是让你这货抠字眼的,说你痛点了,就开始满嘴喷粪骂人是吧,真是个小丑🤡🤡🤡。老登你积点德吧
    kneo
        166
    kneo  
       24 天前
    @CloveAndCurrant 哦?

    我引用你说 wiki 不是真理?

    我说大家 wiki 和你二选一,你开始骂人“老 B 登”。

    我说到让你爸来投票,你开始求我别拿 wiki 抠字眼?你是真怕他不选你啊?

    我都被你逗笑了,这是你的职业吗?
    CloveAndCurrant
        167
    CloveAndCurrant  
       24 天前
    @kneo 服了,抠字眼叫引用 Wiki ,真能给你满脸皱纹老年斑的脸贴金😅😅😅?老是撒泼打滚骂人,你目前即使无儿无女,你曾经也是又爹有娘吧?不要满嘴喷粪,积点德不好?怪不得别人总是说“不是老人变坏了,而是坏人变老了”
    kneo
        168
    kneo  
       24 天前
    @CloveAndCurrant 哈哈,您真是定义的神啊。您来定义下我是在“引用 wiki”还是在“抠字眼”?


    https://en.wikipedia.org/wiki/Coroutine#Go

    However, goroutines are not coroutines (for instance, local data does not persist between successive calls).
    CloveAndCurrant
        169
    CloveAndCurrant  
       24 天前
    @kneo goroutines 不是这种传统无栈协程有什么问题吗? goroutines 是有栈协程啊。你以后别说你看过 Wiki 了,真的是在侮辱 Wiki
    kneo
        170
    kneo  
       24 天前
    @CloveAndCurrant 哈哈,光会说,您倒是引用一个 wiki 说“goroutines 是有栈协程”给我看看啊。需要我帮你吗?
    CloveAndCurrant
        171
    CloveAndCurrant  
       24 天前
    @kneo 你能不能先把你引用的 Wiki 看懂啊😆😆?需要我一口口的喂你?
    purplecity
        172
    purplecity  
       24 天前
    java 就是垃圾中的垃圾
    kneo
        173
    kneo  
       24 天前
    @CloveAndCurrant 嘴硬。
    CloveAndCurrant
        174
    CloveAndCurrant  
       24 天前
    @kneo 无知,Wiki 没看懂就火急火燎跟别人对线🤣🤣🤣
    bli22ard
        175
    bli22ard  
       24 天前   ❤️ 1
    我不认为这个测试方法不专业,因为它能说明,在这个 sleep 场景下,goroutine 内存占比确实比 java 的 virtual thread 占用高。
    我觉的叫 goroutine 为协程并没有什么问题,可能翻译得不够准确,但是当你说 go 协程,别人知道你指的是 goroutine 就可以了,没有什么大问题。

    不过我觉得这个测试可能并不能很好的说明问题,不管是 goroutine ,rust 的 tokio ,或者是 c#的 async/await ,它们本质上解决两个问题,一个 io 就绪回调,一个 sleep 就绪回调。但是要实现这种回调,需要在资源没就绪时候,保存现场,腾出 cpu 给其他任务运行,保存完毕后,其他任务运行,资源就绪后,就有了再次被执行的机会。这个内存消耗主要在保存现场需要的内存上。由于这些异步框架实现方式不一,不排除有些实现,检查测试代码不需要保存现场,直接定时器回调实现,从而节省内存。
    一种测试方法是通过每个任务去发起一个 tcp 连接,然后服务端 hold 10s 再返回,测试内存占用可能更能说明实际的问题。

    另外还有就是执行时间问题,因为如果开一个 goroutine 排队执行,内存占用肯定比开 1m goroutine 小🤣


    也有可能 java 的 virtual thread 和 c# async/await 确认非常优秀,全面领先 goroutine ,希望有实力的人从新设计测试方法。
    比较好奇,每个 goroutine 2kb ,到底存的是什么
    最后 @lesismal @kneo 两位高手来围观
    kneo
        176
    kneo  
       24 天前
    @CloveAndCurrant 哈哈,给你机会了。既然你不会我就帮你个忙。

    Wiki 中很明确的提到了 stackful 和 stackless:

    https://en.wikipedia.org/wiki/Coroutine#Definition_and_types

    > Besides that, a coroutine implementation has 3 features:
    > - ...
    > - ...
    > - whether a coroutine is able to suspend its execution from within nested function calls. Such a coroutine is a stackful coroutine. One to the contrary is called stackless coroutines, where unless marked as coroutine, a regular function can't use the keyword yield.

    同时也举例了:

    https://en.wikipedia.org/wiki/Coroutine#Lua

    > Lua has supported first-class stackful asymmetric coroutines since version 5.0 (2003), in the standard library coroutine.

    很明显,有栈协程和无栈协程都是协程,不是加个定语就可以随便用的。Wiki 也不是连个有栈无栈都不懂的“过时”信息。

    同一页面也明确指出了:

    https://en.wikipedia.org/wiki/Coroutine#Go

    > However, goroutines are not coroutines (for instance, local data does not persist between successive calls).

    goroutines 不是协程,加再多定语也没用。

    这个总结不是给 @CloveAndCurrant 这种人看的,他除了嘴硬啥也不懂。但我相信其他人看了自然能明辨是非。

    我不相信 @CloveAndCurrant 能再给出任何有意义的回复了。不和他聊了。
    kneo
        177
    kneo  
       24 天前
    @bli22ard

    > 比较好奇,每个 goroutine 2kb ,到底存的是什么
    > 最后 @lesismal @kneo 两位高手来围观

    见笑了,谈不上高手。

    我的理解这 2KB 就是预分配的栈大小。过小的初始栈可能会导致频繁的内存重新分配。
    2KB 可能是 go 团队在性能测试之后得到的一个比较好的默认值。这个值也是经过多次调整的:

    https://stackoverflow.com/questions/22326765/go-memory-consumption-with-many-goroutines

    > in Go 1.4: the minimum size of the goroutine stack has decreased from 8Kb to 2Kb.
    > https://github.com/golang/go/blob/bbd25d26c0a86660fb3968137f16e74837b7a9c6/src/runtime/stack.go#L72:
    _StackMin = 2048


    也许可以把 2KB 改成 1KB 重新编译一个 go 版本,内存测试的结果就和 Java 一样了。当然前提是 1KB 对这个测试来说够用,够不够用我也不知道……

    当然真实环境大家确实不在意一个 goroutine 占用了 2KB 是不是太多。但是这个测试至少让我们知道了这个 2KB 的存在。程序员总是对这种小细节感兴趣的。
    orioleq
        178
    orioleq  
       24 天前 via iPhone
    @CloveAndCurrant 说实话你骂得才叫撒泼打滚 @kneo 即使骂人也是有逻辑的,词还蛮新鲜…
    CloveAndCurrant
        179
    CloveAndCurrant  
       24 天前
    @kneo “There is no single precise definition of coroutine”,现代协程概念早就超出这个范围,抠字眼 Wiki 还看不懂😅😅😅
    CloveAndCurrant
        180
    CloveAndCurrant  
       24 天前
    @orioleq 知道了,你是个“受”😂😂😂😂
    grzhan
        181
    grzhan  
       24 天前
    @kneo 我觉得整体的交流讨论本身还是有益的,像我之前也一直把 goroutine 称呼为协程( coroutine ),虽然我知道现在 goroutine 是抢占式的,但这么叫也说明我自己对于计算机系统的理解不够深刻。

    就中文互联网而言,看到的诸多讨论、书籍,包括我之前面试字节的时候面试官都直接将 goroutine 称为“有栈协程”,这个偏差认知在国内应该是很普遍深入了,但反而是有引导到正确认知的价值,后续我应该也会尽自己所能去传播这类正确的知识。
    xiaocaiji111
        182
    xiaocaiji111  
       23 天前
    tonybai 大佬的公众号都发了,让 gopher 正视缺陷,本来就没有不公,大家都是相同条件,基本一致的写法,比较就是要控制变量,用官方提供的用法来实现,但是有些人非要弄池化什么的,搞第三方优化,那对其他语言岂不是也不公平了。
    bli22ard
        183
    bli22ard  
       23 天前
    @kneo
    > 我的理解这 2KB 就是预分配的栈大小。过小的初始栈可能会导致频繁的内存重新分配。
    > 2KB 可能是 go 团队在性能测试之后得到的一个比较好的默认值。这个值也是经过多次调整的:

    更本质的是,为什么需要这个 2kb 内存,这 2kb 存的什么东西,这个期待有实力的人研究一下 runtime 。
    kneo
        184
    kneo  
       23 天前 via Android
    @bli22ard

    > 更本质的是,为什么需要这个 2kb 内存,这 2kb 存的什么东西,这个期待有实力的人研究一下 runtime 。

    2KB 就是栈啊……和其他语言没有本质区别。函数的参数,变量,闭包之类的。不过正常情况 Java 在栈上存对象只存指针,Go 因为有 struct ,相比 Java ,栈内存的使用频率会更多点,预分配的大一点也是合理的。
    bli22ard
        185
    bli22ard  
       23 天前
    @kneo 就启动 goroutine ,不管是堆还是栈,没搞懂它有什么存,如果是参数,变量,为什么要是 2kb ,而不是这些参数、变量的实际大小
    kneo
        186
    kneo  
       23 天前 via Android
    @bli22ard

    > 就启动 goroutine ,不管是堆还是栈,没搞懂它有什么存

    启动 goroutine ,实际上是你用 goroutine 加载了一个函数。栈就是这个函数的运行环境。哪怕是一个空函数,只要没被优化掉,也是一个函数调用,也必须有栈。

    类似的,你在其它语言里启动一个线程也需要提供一个可运行函数(或者等价物)。这不是 GO 专有的概念。

    > 如果是参数,变量,为什么要是 2kb ,而不是这些参数、变量的实际大小

    栈的特点就是动态增长。因为函数调用需要把子函数的存储也分配到这个栈上。函数的嵌套越多,需要的内存就越多。

    栈空间不够用就需要重新分配内存并且把旧的栈数据复制过去。(这里指的是虚拟线程,原生线程收到操作系统限制可能无法修改栈大小。)

    如果初始化栈过小,频繁的扩容会影响性能。

    初始栈过大,会造成内存浪费。

    2KB 是一个性能权衡的结果。

    您可能需要找一些函数调用和栈内存的示意图看一下。不一定非是 GO 的。
    rtu
        187
    rtu  
       23 天前
    这个帖子能有这么激烈的碰撞说明 go 和 java 还是能混口饭吃的,很不错啦
    Plumbiu
        188
    Plumbiu  
       23 天前
    nodejs delay 改成 (n) => new Promise((r) => setTimeout(r, n)) 试试?
    hbdjd
        189
    hbdjd  
       22 天前
    真就没人聊 C#吗 😅
    Metatron7
        190
    Metatron7  
       22 天前
    真实,尽管 C#无论在 JIT 还是 AOT 表现都很好,也没多少讨论的
    lysShub
        191
    lysShub  
       22 天前
    bli22ard
        192
    bli22ard  
       21 天前
    @kneo

    > 栈的特点就是动态增长。因为函数调用需要把子函数的存储也分配到这个栈上。函数的嵌套越多,需要的内存就越多。

    > 栈空间不够用就需要重新分配内存并且把旧的栈数据复制过去。(这里指的是虚拟线程,原生线程收到操作系统限制> 可能无法修改栈大小。)

    > 如果初始化栈过小,频繁的扩容会影响性能。

    > 初始栈过大,会造成内存浪费。

    > 2KB 是一个性能权衡的结果。

    > 您可能需要找一些函数调用和栈内存的示意图看一下。不一定非是 GO 的。

    我找了一些函数调用和栈内存的示意图看了下,2kb 的栈空间是用来保存现场的,go 做了一次预分配,在 op 提到的测试方法,这样预分配在内存占用方面确实会吃亏,反而动态堆分配的会占优势。
    1  2  
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1068 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 19:46 · PVG 03:46 · LAX 11:46 · JFK 14:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.