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

298 天前
 hez2010

去年有一个 “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 万个并发任务:

10 万个并发任务:

100 万个并发任务:

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 更轻量。

10889 次点击
所在节点    程序员
193 条回复
james122333
297 天前
@kneo

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

只有带入过多感情,认为有些数值高的赢了,某些数值低的好像输了,才会纠结“公平”。
lesismal
297 天前
@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
297 天前
@james122333

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

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

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

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

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

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

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

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

作者就是随手撸几段代码测一下,我看到的是单纯的程序员的快乐。不要阴谋论啦。
lesismal
297 天前
@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
297 天前
@james122333 Go 运行时不是会调度 goroutine 吗?

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

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

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

你就是个傻逼,block 了,也别回了

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

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

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

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

© 2021 V2EX