运行 100 万个并发任务究竟需要多少内存?

Laf 公众号已接入了 AI 绘画工具 Midjourney,可以让你轻松画出很多“大师”级的作品。同时还接入了 AI 聊天机器人,支持 GPT、Claude 以及 Laf 专有模型,可通过指令来随意切换模型。欢迎前来调戏👇

2c0fc7655bb9ed6426f586d0c76d9e12.png

d89c3b99c4abc79b5da82373de7e0971.png

<<< 左右滑动见更多 >>>

🌐 原文链接:https://pkolaczk.github.io/memory-consumption-of-async/

本文深入研究了诸如 Rust、Go、Java、C#、Python、Node.js 和 Elixir 等流行编程语言在异步和多线程编程中的内存消耗对比

前段时间我对几个设计处理海量网络连接的应用程序进行了性能评估。我发现它们在内存消耗上差异巨大,有时甚至超过了 20 倍。某些程序仅消耗略超过 100 MB 内存,而其他程序在处理 10k 连接时内存消耗了将近 3GB。这些程序都相当复杂,且特性各不相同,因此难以直接比较并得出有意义的结论。这明显不公平。因此,我决定创建一个合成基准测试来进行公平地对比。

基准测试

我将用各种编程语言来实现以下逻辑:

启动 N 个并发任务,其中每个任务等待 10 秒,所有任务完成后程序退出。任务的数量由命令行参数控制。

在 ChatGPT 的帮助下,我可以在几分钟内编写出这样的程序,即使对我来说并不常用的编程语言也可以轻松应对。为了方便大家,所有的基准测试代码都发布在我的 GitHub 上[1]

Rust

我用 Rust 创建了 3 个程序。第一个使用传统的线程,其核心代码如下:

let mut handles = Vec::new();
for _ in 0..num_threads {let handle = thread::spawn(|| {thread::sleep(Duration::from_secs(10));});handles.push(handle);
}
for handle in handles {handle.join().unwrap();
}

另外两个版本则使用了异步,一个使用 tokio,另一个使用 async-std。以下是 tokio 版本的核心代码:

let mut tasks = Vec::new();
for _ in 0..num_tasks {tasks.push(task::spawn(async {time::sleep(Duration::from_secs(10)).await;}));
}
for task in tasks {task.await.unwrap();
}

async-std 版本与上面的代码非常类似,我就不在这里引述了。

Go

在 Go 语言中,goroutines 是并发的基础模块。我们不会单独等待它们完成,而是使用 WaitGroup

var wg sync.WaitGroup
for i := 0; i < numRoutines; i++ {wg.Add(1)go func() {defer wg.Done()time.Sleep(10 * time.Second)}()
}
wg.Wait()

Java

Java 一般使用的都是线程,但是 JDK 21 提供了虚拟线程的预览版,这是一个与 goroutines 类似的概念。因此,我创建了两个基准测试的变体。

传统线程版本如下:

List<Thread> threads = new ArrayList<>();
for (int i = 0; i < numTasks; i++) {Thread thread = new Thread(() -> {try {Thread.sleep(Duration.ofSeconds(10));} catch (InterruptedException e) {}});thread.start();threads.add(thread);
}
for (Thread thread : threads) {thread.join();
}

虚拟线程版本与线程类似,只是创建线程的方法略有不同:

List<Thread> threads = new ArrayList<>();
for (int i = 0; i < numTasks; i++) {Thread thread = Thread.startVirtualThread(() -> {try {Thread.sleep(Duration.ofSeconds(10));} catch (InterruptedException e) {}});threads.add(thread);
}
for (Thread thread : threads) {thread.join();
}

C#

C# 和 Rust 一样,对 async/await 的支持都比较完善:

List<Task> tasks = new List<Task>();
for (int i = 0; i < numTasks; i++)
{Task task = Task.Run(async () =>{await Task.Delay(TimeSpan.FromSeconds(10));});tasks.Add(task);
}
await Task.WhenAll(tasks);

Node.JS

Node.JS 也一样:

const delay = util.promisify(setTimeout);
const tasks = [];for (let i = 0; i < numTasks; i++) {tasks.push(delay(10000);
}await Promise.all(tasks);

Python

Python 3.5 引入了 async/await,因此我们可以这样写:

async def perform_task():await asyncio.sleep(10)tasks = []for task_id in range(num_tasks):task = asyncio.create_task(perform_task())tasks.append(task)await asyncio.gather(*tasks)

Elixir

最后,我还编写了一个使用 Elixir 语言的版本,该语言以其异步能力而闻名:

tasks =for _ <- 1..num_tasks doTask.async(fn ->:timer.sleep(10000)end)endTask.await_many(tasks, :infinity)

测试环境

  • 硬件:Intel(R) Xeon(R) CPU E3-1505M v6 @ 3.00GHz

  • 操作系统:Ubuntu 22.04 LTS, Linux p5520 5.15.0-72-generic

  • Rust:1.69

  • Go:1.18.1

  • Java:OpenJDK “21-ea” build 21-ea+22-1890

  • .NET:6.0.116

  • Node.JS:v12.22.9

  • Python:3.10.6

  • Elixir:Erlang/OTP 24 erts-12.2.1, Elixir 1.12.2

所有程序均在 release 模式下运行(如有该模式)。其他选项保持默认。

结果

最小内存占用

让我们从小处着眼。考虑到每种运行环境都需要一定的内存,因此我们先只启动一个任务。

25b5de255f61c2ac4dc98bfee3b27458.png
图1:启动一个任务所需的最高内存

此图表明,程序可以明显分为两类。Go 与 Rust 程序,作为编译成静态本机二进制文件的形式,消耗的内存非常少。相反,运行在管理平台或通过解释器运行的程序需要更多内存,尽管在这种情况下 Python 的表现相当出色。两类程序之间的内存占用大约相差一个数量级

令我惊讶的是,.NET 的内存占用最大,但我想或许可以通过调整一些设置来解决。如果您有任何解决方案,欢迎在评论区分享。我在调试模式与发布模式之间并未发现显著差异。

10k 并发任务

c516244f84fbe8e0020d00eba7587de8.png
图2:启动 10,000 个任务所需的最高内存

这张图有几个意料之外的结论!大家可能都预测线程会是这项基准测试的落败者。对于 Java 线程的确如此,因为它们耗费了近 250MB 的 RAM。然而,Rust 使用的本机 Linux 线程似乎非常轻量级,即使在 10k 线程的情况下,其内存消耗仍然低于许多其他运行环境的空闲内存消耗。异步任务或虚拟线程可能比本机线程更轻,但在仅有 10k 任务的情况下,我们并未看到这种优势。我们需要更多的任务来进行对比。

另一个出乎意料的是 Go。Goroutines 应该非常轻量,然而实际上它们消耗的内存超过了 Rust 线程所需内存的 50%。老实说,我原本以为 Go 会有更大的优势。因此,我得出的结论是,在 10k 并发任务的情况下,线程仍然是一种相当有竞争力的选择。Linux 内核在这方面表现得相当出色。

在之前的基准测试中,Go 与 Rust 异步相比具有微小的优势,但现在它已经失去了这个优势,并且消耗的内存比最优秀的 Rust多了 6 倍以上。同时,它也被 Python 超越了。

最出乎意料的是,在 10k 并发任务的情况下,.NET 的内存消耗与空闲内存使用相比并没有显著增加。可能它只是利用了预分配的内存,或者其空闲内存使用率非常高,10k 并发任务对它来说太少了,不足以产生重大影响。

100k 并发任务

我无法在我的系统上启动10万个线程,因此只能放弃线程基准测试。也许可以通过调整系统设置来解决,但是尝试了一个小时后我还是放弃了。所以在 100k 并发任务的情况下,线程可能并非理想选择。

db342992f472a558e2244a3dc9edcbaa.png
图3:启动 10 万个任务所需的最高内存

现在,我们看到了一些显著变化。Go 和 Python 消耗的内存迅速增长,而 Java 虚拟线程,Rust async 和 Node.JS 保持相对较低的内存消耗。我们还可以看到 .NET 在这个基准测试中的优秀表现,它的内存使用量仍然没有增加,也没有阻塞主循环,太厉害了!

100w 并发任务

最后,我尝试增加任务的数量,试图启动一百万个任务。在这个数量级下,我们可以清晰地看到一些运行环境的真正优势。

b8ff9747cb91ef62c1f15e4f3c64ed0c.png
图4:启动100万个任务所需的最高内存

在这个数量级下,只有 Rust async(无论是 tokio 还是 async-std)、Java 虚拟线程和 .NET 才能运行。Go,Python 和 Node.JS 都耗尽了我的系统的 16GB 内存,而且并未完成基准测试。

Go 与其他语言之间的差距越来越大。现在,Go 的比分比最高分少了 12 倍。它比 Java 的分数也少了两倍多,这与 “JVM 占用内存较多、Go 轻量”的一般认识相矛盾。

这也表明 Java 虚拟线程和 Rust async 在内存使用效率上旗鼓相当

结论

如果你需要处理的并发任务数量超过 100,000,那么 Java 虚拟线程和 Rust async 可能是最好的选择。如果你的任务数量在这个范围之下,那么线程(至少是 Rust 和 Linux 本地线程)可能仍然是一个具有竞争力的选择,尤其是在你想要避免引入异步编程复杂性的情况下。

另一方面,如果你正在开发一个需要处理大量并发任务的系统,那么选择支持异步编程的语言和运行时可能是必要的。在这种情况下,Rust 和 Java 可能是非常好的选择,因为它们在这些基准测试中表现优秀。

然而,请记住,这只是一个非常简单的基准测试,它不能考虑到所有可能影响真实世界应用程序的因素,如 CPU 使用,I/O 操作,垃圾收集等。因此,在选择编程语言和运行时时,需要综合考虑这些因素。

引用链接

[1]

发布在我的 GitHub 上: https://github.com/pkolaczk/async-runtimes-benchmarks

关于 Laf

Laf 是一款为所有开发者打造的集函数、数据库、存储为一体的云开发平台,助你像写博客一样写代码,随时随地发布上线应用!3 分钟上线 ChatGPT 应用!

🌟GitHub:https://github.com/labring/laf

🏠官网(国内):https://laf.run

🌎官网(海外):https://laf.dev

💻开发者论坛:https://forum.laf.run

关注 Laf 公众号与我们一同成长👇👇👇

0ffeac7c5ed7859262093bf01478046b.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/256311.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

MyBatis 环境搭建+基本使用

目录 MyBatis创建MyBatis环境搭建MyBatis模式开发MyBatis 获取动态参数&#xff08;查询操作&#xff09;${} 直接替换#{} 占位符模式替换like查询&#xff08;模糊查询&#xff09;多表查询一对一的表映射一对多的表映射 增、删、改操作改操作删除操作增加操作添加用户添加用户…

Spring事务与事务传播

文章目录 一、什么是事务?二、Spring事务实现编程式事务声明式事务 三、Transactional的使用参数作用Spring事务的隔离级别事务失效的场景Transactional工作原理 四、Spring事务传播机制Spring有哪些事务传播机制&#xff1f; 一、什么是事务? 事务&#xff1a;事务是一组操…

mybatis分页中的报错

1 Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.lang.NoSuchMethodException: com.github.pagehelper.BoundSqlInterceptor.<init>() 出错的原因就是上面的那句话 Error parsing SQL Mapper Configuration…

Mybatis分页方式及实现原理

一、mybatis的4种分页方式(物理分页、逻辑分页) 1、借助Sql语句Q进行分页(物理分页) 2、拦截器分页(物理分页)通过拦截器给sq语句末尾加Eimt语句来查询 3、借助 数组Q进行分页(逻辑分页) 4、RowBounds分页插件实现分页(逻辑分页) 二、mybatis分页的原理 mybatis分页原理是&…

Ora提示词版ChatGPT机器人

Ora可以自己创建一个ChatGPT机器人&#xff0c;可以设置自己的提示词例如我创建的AI佛祖https://ora.ai/aesthetic-red-nfa4/ai%E4%BD%9B%E7%A5%96 提示词 创建机器人的时候&#xff0c;需要设定自己的提示词&#xff0c;例如&#xff1a; 假设你是佛祖&#xff0c;名字叫做释迦…

mybatis分页查询插件

1.引入jar包 <dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.3.0</version></dependency> 2.在mybatis的核心配置文件mybatis.xml中配置分页插件 3.使用pageHec publi…

Mybatis分页查询——四种传参方式

目录 相关导读 一、顺序传参 1. 持久层接口方法 2. UserMapper.xml映射文件新增标签 3. 新增测试方法 4. 运行结果 二、param传参 1. 持久层接口方法 2. UserMapper.xml映射文件新增标签 3. 新增测试方法 4. 运行结果 三、自定义POJO类传参 1. 自定义POJO类 2. 持…

MyBatis分页插件

目录 分页插件 Mybatis插件典型适用场景 实现思考 第一个问题 第二个问题 自定义分页插件 分页插件使用 添加pom依赖 插件注册 调用 代理和拦截是怎么实现的 PageHelper 原理 分页插件 MyBatis 通过提供插件机制&#xff0c;让我们可以根据自己的需要去增强MyBati…

Mybatis——分页

1.为什么要分页&#xff1f; 减少数据的处理量使用Limit分页 select * from user limit startIndex,pageSize;使用Mybatis实现分页&#xff0c;核心SQL 1.数据库文件-db.properties drivercom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/mybatis?useSSLfalse&…

Mybatis实现分页的三种方式

文章目录 1、Limit实现分页2、RowBounds分页&#xff08;不建议使用&#xff09;3、MyBatis分页插件PageHelper&#xff08;了解即可&#xff09; 1、Limit实现分页 sql语句 SELECT * from user limit startIndex,pageSize简单示例&#xff1a; user表 查询一&#xff1a;从第…

Mybatis的四种分页方式详解

LIMIT关键字 mapper代码 select * from tb_user limit #{pageNo}, #{pageSize} 业务层直接调用 public List findByPageInfo(PageInfo info) { return userMapper.selectByPageInfo(info); } 3&#xff0c;优点 灵活性高&#xff0c;可优化空间大 mysql分页语句优化 4&…

mybatis实现分页的几种方式

本文目录 借助数组进行分页借助Sql语句进行分页拦截器分页RowBounds实现分页 借助数组进行分页 原理&#xff1a;进行数据库查询操作时&#xff0c;获取到数据库中所有满足条件的记录&#xff0c;保存在应用的临时数组中&#xff0c;再通过List的subList方法&#xff0c;获取到…

【分布式应用】ELK企业级日志分析系统

一、ELK 简介 ELK平台是一套完整的日志集中处理解决方案&#xff0c;将 ElasticSearch、Logstash 和 Kiabana 三个开源工具配合使用&#xff0c; 完成更强大的用户对日志的查询、排序、统计需求。 1.1 ELK各组件介绍 ElasticSearch&#xff1a; 是基于Lucene&#xff08;一个…

Rust每日一练(Leetday0016) 全排列I\II、旋转图像

目录 46. 全排列 Permutations &#x1f31f;&#x1f31f; 47. 全排列 II Permutations II &#x1f31f;&#x1f31f; 48. 旋转图像 Rotate Image &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专…

win10操作系统官网如何下载ios境像文件安装操作系统

1.打开官网 2.立即下载工具 3.正在准备进行工作 4.接收条款 5.根据需求选择安装合适的位置 6.等待创建成功 7.右键选择装载 8.双击安装setup.exe文件 8 使用微Pe安装过程中发现需要联网更新解决 winR 然后cmd 输入 OOBE\BYPASSNRO 9 或者关闭此进程 电脑不要插网线&#xff…

虚拟机系统iso镜像下载_如何下载正版系统镜像

许多网友想装系统&#xff0c;可是不会装(后期会推装系统的文章)&#xff0c;而且网上的系统有的装有全家桶&#xff0c;有的又有捆绑软件&#xff0c;严重的还有病毒。那么&#xff0c;在哪里去下载正版&#xff0c;安全的系统镜像呢 "MSDN&#xff0c;我告诉你"这个…

自定义IOS系统弹框

写在之前 系统的弹框虽然很好看&#xff0c;但是有时候无法完全满足UI设计的需求&#xff0c;比如说中间要显示一个输入框&#xff0c;或者要放置一张图片&#xff0c;这里介绍一个很早之前的自定义弹框库&#xff0c;这个自定义弹框sdk写的很细致&#xff0c;定制性很强&…

华为vivo小米魅族oppo荣耀安卓手机刷IOS苹果系统

现在的刷机教程层出不穷&#xff0c;但是没有哪个大神可以开发出一款安卓刷IOS的工具&#xff0c;使得安卓手机或者安卓平板变成IOS苹果系统&#xff0c;这个工具除了可以刷安卓手机也可以刷安卓平板&#xff0c;网友实测&#xff0c;如果设备不支持的话软件会提醒不支持&#…

android高仿ios11系统,安卓仿ios11桌面全套文件

安卓手机想要苹果11的桌面的话&#xff0c;安卓仿ios11桌面全套文件app就可以帮您实现哦。安卓仿ios11桌面全套文件app是一款超级优质的手机桌面工具软件&#xff0c;平台上面拥有海量的壁纸资源&#xff0c;大家可以任意选取哦。 应用说明 修改负一屏效果 修改dock栏模糊度 修…

软件配置 | ios系统Clion下载、安装、配置环境

软件配置 软件配置 | ios系统Clion下载、安装、配置环境 软件配置下载、安装gClion 调试参考链接下载、安装调试 本文总结ios系统下Clion下载、安装、配置环境过程和可能碰到的问题。 因为是在私人电脑&#xff08;ios&#xff09;上配置的Clion&#xff0c;我还不太熟悉自己的…