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

news2024/12/24 9:05:10

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 do
        Task.async(fn ->
            :timer.sleep(10000)
        end)
    end

Task.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://www.coloradmin.cn/o/591760.html

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

相关文章

Tomcat文件夹属性

Tomcat安装完成后&#xff0c;其安装目录下包含bin、conf、lib、logs、temp、webapps、work等子目录&#xff0c;各个子目录简介如下&#xff1a; &#xff08;1&#xff09;bin目录。主要存放Tomcat的命令文件。&#xff08;解压缩版点击bin下的startup.bat&#xff0c;即可运…

4.2 字节流与字符流

在Java中&#xff0c;有两种基本的数据流类型&#xff1a;字节流和字符流。字节流处理原始二进制数据&#xff0c;而字符流处理Unicode字符。本章节我们将学习字节流与字符流的基本概念以及如何使用它们进行文件的输入输出操作。 4.2.1 字节流 字节流处理原始二进制数据&…

打造音视频极致消费体验

在观看视频时&#xff0c;用户最看重的是什么呢&#xff1f;清晰度&#xff1f;流畅度&#xff1f;还是播放时的稳定性&#xff1f;作为视频厂商&#xff0c;不仅要考虑到常见的指标&#xff0c;一些关乎用户体验的隐藏性指标也需要重点关注。如何持续升级优化代码并在成本和用…

JointJS+ v3.7 Crack

JointJS v3.7 改进了对 SVG 上下文中的外部对象的支持。 2023 年 5 月 30 日 - 16:00 新版本 特征 改进了对外部对象 (HTML) 的支持- 外部对象已成为 Web 开发的标准&#xff0c;JointJS 现在已经在 SVG 上下文中引入了对外部对象的全面且功能齐全的支持。这意味着您现在可以在…

工作积极主动分享,善于业务沟通

工作积极主动分享&#xff0c;善于业务沟通 目录概述需求&#xff1a; 设计思路实现思路分析1.工作积极主动承担责任2.善于沟通3.一起常常lauch 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;…

【JavaSE】Java基础语法(三十九):网络编程入门

文章目录 1. 网络编程概述2. 网络编程三要素3. IP地址4. InetAddress5. 端口和协议 1. 网络编程概述 计算机网络 是指将地理位置不同的具有独立功能的多台计算机及其外部设备&#xff0c;通过通信线路连接起来&#xff0c;在网络 操作系统&#xff0c;网络管理软件及网络通信协…

MyBatis 环境搭建+基本使用

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

chatgpt赋能python:Python中的英文单词

Python中的英文单词 Python是一种流行的编程语言&#xff0c;它具有人类易读性、功能强大、支持多种编程范例等特点。Python中包含着大量的英文单词&#xff0c;这些单词在Python编程中极为重要&#xff0c;因为它们直接影响代码的可读性和理解难度。本文将介绍一些最常用的Py…

Go开发学习 | 如何使用Gomail.v2模块包发送邮箱验证码消息及附件学习记录

欢迎关注「全栈工程师修炼指南」公众号 点击 &#x1f447; 下方卡片 即可关注我哟! 设为「星标⭐」每天带你 基础入门 到 进阶实践 再到 放弃学习&#xff01; “ 花开堪折直须折&#xff0c;莫待无花空折枝。 ” 作者主页&#xff1a;[ https://www.weiyigeek.top ] 博客&…

vulhub-Jarbas(易)

打靶练习Jarbas 0x00 部署0x01 信息收集&#xff1a;端口扫描、服务发现0x02 路径爬取0x03 反弹shell0x04 内网信息收集0x05 crontab定时任务提权0x06 总结 0x00 部署 靶机&#xff1a;下载地址 宿主机&#xff1a;kali2021版本 0x01 信息收集&#xff1a;端口扫描、服务发现…

《计算机组成原理》唐朔飞 第9章 控制单元的功能 - 学习笔记

写在前面的话&#xff1a;此系列文章为笔者学习计算机组成原理时的个人笔记&#xff0c;分享出来与大家学习交流。使用教材为唐朔飞第3版&#xff0c;笔记目录大体与教材相同。 网课 计算机组成原理&#xff08;哈工大刘宏伟&#xff09;135讲&#xff08;全&#xff09;高清_…

git (本地仓库)和(远程仓库)之间的代码推送:013

这里先说明一下循序&#xff1a; 1. 创建(远程仓库)和(本地仓库) 2. 创建(远程仓库)和(本地仓库)之间的链接 3. 将(本地仓库)的代码推通过命令送到(远程仓库)&#xff1b;将(本地仓库)的代码通过(TortoiseGit小乌龟)推送到(远程仓库) 1. 创建(远程仓库)和(本地仓库)&#xff0c…

PHP异步:在PHP中使用 fsockopen curl 实现类似异步处理的功能

PHP从主流来看&#xff0c;是一门面向过程的语言&#xff0c;它的最大缺点就是无法实现多线程管理&#xff0c;其程序的执行都是从头到尾&#xff0c;按照逻辑一路执行下来&#xff0c;不可能出现分支&#xff0c;这一点是限制php在主流程序语言中往更高级的语言发展的原因之一…

C++实现sqlite单表增删改查的详细步骤

1.环境准备 coding之前需要先安装好C的集成开发环境&#xff0c; 我这里选择的是Visual Studio 2022&#xff0c;本来想使用CLion的&#xff0c; 但是破解太麻烦&#xff0c;懒得整了。 Visual Studio 2022 2.项目创建及编码 启动visual studio, 点击创建项目&#xff0c;选…

《MYSQL必知必会》读书笔记1

目录 行 主键 MYSQL工具 使用MYSQL 连接 检索数据 检索&#xff08;SELECT&#xff09; 限制结果&#xff08;LIMIT&#xff09; 排序检索&#xff08;ORDER BY&#xff09; 过滤数据&#xff08;WHERE&#xff09; 过滤数据&#xff08;AND、OR&#xff09; 通配符…

软件测试总结

软件生命周期(SDLC)的六个阶段 1、问题的定义及规划 此阶段是软件开发方与需求方共同讨论&#xff0c;主要确定软件的开发目标及其可行性。 2、需求分析 在确定软件开发可行的情况下&#xff0c;对软件需要实现的各个功能进行详细分析。需求分析阶段是一个很重要…

ML | 6 支持向量机

ML | 6 支持向量机 文章目录 ML | 6 支持向量机SVM介绍线性不可分数据线性可分数据 寻找最大间隔分类器求解的优化问题 SMO高效优化算法简化版SMO处理小规模数据集伪代码程序清单 完整Platt SMO 算法加速优化完整 Platt SMO的支持函数完整Platt SMO算法中的优化例程完整Platt S…

记录--Vue3自定义一个Hooks,实现一键换肤

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 核心 使用CSS变量, 准备两套CSS颜色, 一套是在 light模式下的颜色,一套是在dark模式下的颜色dark模式下的 CSS 权重要比 light 模式下的权重高, 不然当我们给html添加自定义属性[data-themedark]的时候…

CVPR 2023 | 南大王利民团队提出LinK:用线性核实现3D激光雷达感知任务中的large kernel...

点击下方卡片&#xff0c;关注“CVer”公众号 AI/CV重磅干货&#xff0c;第一时间送达 点击进入—>【Transformer】微信交流群 【CVPR 2023】LinK&#xff1a;用线性核实现3D激光雷达感知任务中的large kernel 本文介绍我们媒体计算研究组&#xff08;MCG&#xff09;在3D激…

chatgpt赋能python:Python中的提取函数——数据清洗中必不可少的利器

Python中的提取函数——数据清洗中必不可少的利器 数据清洗是数据分析过程中不可或缺的一步&#xff0c;而Python中的提取函数则是数据清洗中必不可少的利器。本文将重点介绍一些Python中常用的提取函数&#xff0c;以帮助数据分析师更好地应对实际问题。 什么是提取函数&…