从技术上讲,为啥 Erlang 中的进程比 OS 线程更高效?

Posted

技术标签:

【中文标题】从技术上讲,为啥 Erlang 中的进程比 OS 线程更高效?【英文标题】:Technically, why are processes in Erlang more efficient than OS threads?从技术上讲,为什么 Erlang 中的进程比 OS 线程更高效? 【发布时间】:2011-02-12 01:49:36 【问题描述】:

Erlang 的特点

来自Erlang Programming (2009):

Erlang 并发是快速且可扩展的。它的进程是轻量级的,因为 Erlang 虚拟机不会为每个创建的进程创建一个 OS 线程。它们在 VM 中创建、调度和处理,独立于底层操作系统。因此,进程创建时间是微秒级的,并且与同时存在的进程数无关。将此与 Java 和 C# 进行比较,后者为每个进程创建一个底层操作系统线程:您将获得一些非常有竞争力的比较,Erlang 大大优于这两种语言。

来自 Concurrency oriented programming in Erlang (pdf)(slides)(2003):

我们观察到创建一个 Erlang 进程所花费的时间是恒定的 1µs 到 2,500 个进程;此后,对于多达 30,000 个过程,它增加到大约 3µs。 Java 和 C# 的性能显示在图的顶部。对于少量进程,创建一个进程大约需要 300µs。创建超过两千个进程是不可能的。

我们看到,对于多达 30,000 个进程,两个 Erlang 进程之间发送消息的时间约为 0.8µs。对于 C#,每条消息大约需要 50µs,直到最大进程数(大约 1800 个进程)。 Java 更糟糕的是,对于多达 100 个进程,每条消息大约需要 50µs,此后当有大约 1000 个 Java 进程时,它迅速增加到每条消息 10ms。

我的想法

从技术上讲,我不完全理解为什么 Erlang 进程在生成新进程方面如此高效,并且每个进程的内存占用要小得多。操作系统和 Erlang 虚拟机都必须进行调度、上下文切换以及跟踪寄存器中的值等等......

为什么操作系统线程的实现方式与 Erlang 中的进程不同?他们是否必须支持更多的东西?为什么他们需要更大的内存占用?为什么它们的产卵和交流速度较慢?

从技术上讲,为什么 Erlang 中的进程在生成和通信方面比操作系统线程更有效?为什么不能以同样有效的方式实现和管理操作系统中的线程?为什么操作系统线程的内存占用更大,生成和通信更慢?

更多阅读

Inside the Erlang VM with focus on SMP (2008) Concurrency in Java and in Erlang (pdf) (2004) Performance Measurements of Threads in Java and Processes in Erlang (1998)

【问题讨论】:

在尝试理解假设为真的原因之前,您需要确定是否该假设为真的——例如,有证据支持。您是否有任何同类比较的参考,证明 Erlang 进程实际上比(比如说)最新 JVM 上的 Java 线程更有效?还是直接使用操作系统进程和线程支持的 C 应用程序? (后者对我来说似乎非常非常不可能。前者只是有点可能。)我的意思是,在有限的环境(弗朗西斯科的观点)下,这可能是真的,但我想看看数字。 @Donal:与许多其他绝对陈述一样。 :-) @Jonas:谢谢,但我知道日期(1998-11-02)和 JVM 版本(1.1.6)并停止了。 Sun 的 JVM 在过去 11.5 年 中得到了相当大的改进(大概 Erlang 的解释器也是如此),尤其是在线程领域。 (为了清楚起见,我并不是说这个假设不正确[弗朗西斯科和多纳尔已经指出了为什么 Erland 可以在那里做某事];我说的是不应该从表面上看未经检查。) @Jonas: "...但我猜你可以在 Erlang 中做到这一点..." 这是“猜测”部分,伙计。 :-) 你猜测 Erlang 的进程切换规模超过了数千。您猜测它比 Java 或 OS 线程做得更好。猜测和软件开发并不是一个很好的组合。 :-) 但我想我已经表达了我的观点。 @T.J. Crowder:安装 erlang 并运行 erl +P 1000100 +hms 100,然后输入 _, PIDs = timer:tc(lists,map,[fun(_)->spawn(fun()->receive stop -> ok end end) end, lists:seq(1,1000000)]).,然后等待大约三分钟得到结果。就是这么简单。在我的笔记本电脑上,每个进程需要 140us 和 1GB 的整个 RAM。不过是直接form shell,编译后的代码应该会更好。 【参考方案1】:

有几个影响因素:

    Erlang 进程不是操作系统进程。它们由 Erlang VM 使用轻量级协作线程模型(在 Erlang 级别抢占,但在协作调度的运行时控制下)实现。这意味着切换上下文要便宜得多,因为它们只在已知的受控点切换,因此不必保存整个 CPU 状态(正常、SSE 和 FPU 寄存器、地址空间映射等)。李> Erlang 进程使用动态分配的堆栈,堆栈开始时非常小,并根据需要增长。这允许在不占用所有可用 RAM 的情况下生成数千甚至数百万的 Erlang 进程。 Erlang 曾经是单线程的,这意味着不需要确保进程之间的线程安全。它现在支持 SMP,但同一调度程序/内核上的 Erlang 进程之间的交互仍然非常轻量级(每个内核有单独的运行队列)。

【讨论】:

第二点:如果进程尚未运行,则没有理由为其分配堆栈。另外:可以通过摆弄一个进程的 GC 来玩一些技巧,这样它就不会收集内存。但这是先进的,有些危险:) 第三点:Erlang 强制执行不可变数据,因此引入 SMP 不应影响线程安全。 @nilskp,没错,erlang也是一种函数式编程语言。所以没有“变量”数据。这导致线程安全。 @nilskp:(RE:您对第 3 点发表评论……)尽管语言本身具有不可变的类型系统,但底层实现——消息传递、调度程序等——完全是另一回事。正确和高效的 SMP 支持不仅仅发生在轻轻一按开关。 @rvirding:感谢澄清附录。我冒昧地将您的观点整合到我的答案中。【参考方案2】:

经过更多研究,我找到了 Joe Armstrong 的演示文稿。

来自Erlang - software for a concurrent world (presentation)(13 分钟):

[Erlang] 是一种并发语言——我的意思是线程是编程语言的一部分,它们不属于操作系统。这确实是 Java 和 C++ 等编程语言的问题所在。它的线程不在编程语言中,线程是操作系统中的东西——它们继承了操作系统中的所有问题。其中一个问题是内存管理系统的粒度。 操作系统中的内存管理保护整个内存页面,因此线程的最小尺寸就是页面的最小尺寸。 其实太大了。

如果你给你的机器增加更多内存——你有相同数量的比特来保护内存,所以页表的粒度会增加——你最终会使用 64kB一个你知道运行在几百字节中的进程。

我认为它至少回答了我的一些问题,如果不是全部的话

【讨论】:

相关: ***.com/questions/2267545/… 栈上的内存保护是有原因的。 Erlang 是否只是不通过处理器的 MMU 保护不同执行上下文的堆栈? (只是希望最好?)如果一个线程使用的不仅仅是它的小堆栈怎么办? (是否检查所有堆栈分配以查看是否需要更大的堆栈?堆栈是否可移动?) @Thanatos:Erlang 不允许程序访问内存或摆弄堆栈。所有分配都必须经过托管运行时,包括堆和堆栈。换句话说:硬件保护是无用的,因为它可以防止无论如何都不会发生的事情。该语言是指针安全、堆栈安全、内存安全和类型安全的。一个进程不能使用超过它的“小堆栈”,因为堆栈会根据需要增长。你可能认为它是微小的反面:无限大。 (但懒惰分配。) 你应该看看微软研究院的 Singularity 操作系统。在 Singularity 中,所有代码、内核、设备驱动程序、库和用户程序都以完整的内核权限运行在 ring 0 中。所有代码、内核、设备驱动程序、库和用户程序都运行在一个平面物理地址空间中,没有任何内存保护。团队发现语言做出的保证比 MMU 可以做出的保证要强得多,同时使用 MMU 会使他们的性能损失高达 30%(!!!)。那么,如果您的语言已经使用了 MMU,为什么还要使用它呢? OS/400 操作系统的工作方式相同。所有程序只有一个平面地址空间。并且当今实际使用的大多数语言都具有相同的安全属性(ECMAScript、Java、C♯、VB.NET、php、Perl、Python、Ruby、Clojure、Scala、Kotlin、Groovy、Ceylon、F♯、OCaml、 “Objective-C”的“Objective”部分,“C++”的“++”部分)。如果没有遗留的 C 代码,以及 C++ 和 Objective-C 的遗留特性,我们甚至不再需要虚拟内存。【参考方案3】:

我已经在汇编程序中实现了协程,并测量了性能。

在现代处理器上切换协程(也称为 Erlang 进程)大约需要 16 条指令和 20 纳秒。此外,您通常知道要切换到的进程(例如:在其队列中接收消息的进程可以实现为从调用进程直接切换到接收进程),因此调度程序不会发挥作用,使得这是一个 O(1) 操作。

切换操作系统线程大约需要 500-1000 纳秒,因为您正在调用内核。操作系统线程调度程序可能在 O(log(n)) 或 O(log(log(n))) 时间内运行,如果您有数万甚至数百万个线程,这将开始变得明显。

因此,Erlang 进程更快且可扩展性更好,因为切换的基本操作更快,调度程序运行频率更低。

【讨论】:

【参考方案4】:

Erlang 进程(大约)对应于其他语言中的green threads;进程之间没有操作系统强制的分离。 (很可能存在语言强制分离,但尽管 Erlang 做得比大多数人都好,但这种保护作用较小。)因为它们的重量轻得多,所以它们可以更广泛地使用。

另一方面,OS 线程能够简单地在不同的 CPU 内核上调度,并且(大部分)能够支持独立的 CPU 绑定处理。操作系统进程类似于操作系统线程,但具有更强的操作系统强制分离。这些功能的代价是操作系统线程和(甚至更多)进程更昂贵。


另一种理解差异的方式是这样。假设您要在 JVM 之上编写 Erlang 的实现(不是一个特别疯狂的建议),那么您将使每个 Erlang 进程成为具有某种状态的对象。然后,您将拥有一个运行 Erlang 进程的 Thread 实例池(通常根据主机系统中的内核数量确定大小;这是实际 Erlang 运行时 BTW 中的一个可调参数)。反过来,这将在可用的真实系统资源中分配要完成的工作。这是一种非常简洁的做事方式,但完全依赖于每个单独的 Erlang 进程并没有做太多事情的事实。当然没关系; Erlang 的结构不要求这些单独的进程是重量级的,因为执行程序的是它们的整体。

在许多方面,真正的问题是术语之一。 Erlang 称为进程的事物(与 CSP、CCS 和特别是 π 演算中的同一概念强烈对应)与具有 C 遗产的语言(包括 C++、Java、C# 和许多其他人)调用进程或线程。有一些相似之处(都涉及一些并发执行的概念),但绝对没有等价性。所以当有人对你说“过程”时要小心;他们可能会将其理解为完全不同的东西……

【讨论】:

Erlang 没有接近 Pi 演算。 Pi 演算假设可以绑定到变量的通道上的同步事件。这种概念根本不适合 Erlang 模型。尝试加入微积分,Erlang 更接近这一点,尽管它仍然需要能够本地加入一些消息等等。有一篇名为 JErlang dedicated 的论文(和项目)实现了它。 这完全取决于您对 pi 演算的看法(您可以使用同步通道和缓冲进程对异步通道进行建模)。 您只是说 Erlang 进程是轻量级的,但您并没有解释为什么它们的占用空间更小(是轻量级的)以及为什么它们比 OS 线程具有更好的性能。 @Jonas:对于某些类型的任务(尤其是计算量大的任务),操作系统线程做得更好。请注意,这些通常不是使用 Erlang 的任务。 Erlang 专注于处理大量简单的通信任务。这样做的一个好处是,在一组任务处理一项工作并等待结果的情况下,这一切都可以在单个处理器上的单个操作系统线程中完成,这比有上下文切换。 理论上,通过使用非常小的堆栈并仔细控制分配的其他线程特定资源的数量,您也可以使 OS 线程变得非常便宜,但在实践中这是相当成问题的。 (预测堆栈需求有点像魔法。)因此,OS 线程被特别设计为在它们较少(按 CPU 内核数量的顺序)并且它们做的更重要的情况下是最佳的每个处理的数量。【参考方案5】:

我认为 Jonas 想要一些数据来比较 OS 线程和 Erlang 进程。 Programming Erlang 的作者 Joe Armstrong 不久前测试了 Erlang 进程生成到 OS 线程的可伸缩性。他用 Erlang 编写了一个简单的 Web 服务器,并针对多线程 Apache 进行了测试(因为 Apache 使用 OS 线程)。有一个旧网站的数据可以追溯到 1998 年。我只设法找到该网站一次。所以我不能提供链接。但是信息就在那里。该研究的主要观点表明,Apache 最多只能处理不到 8K 进程,而他的手写 Erlang 服务器处理了 10K+ 进程。

【讨论】:

我想你说的是这个:sics.se/~joe/apachevsyaws.html 但我问的是 erlang 是如何使线程与 kerlenl 线程相比如此高效的。 @Jonas 链接已失效。最后一张快照是here 文章说:“Apache 在大约 4,000 个并行会话时死掉。Yaws 在超过 80,000 个并行连接时仍在运行。” 查看完整文章citeseerx.ist.psu.edu/viewdoc/… 事实上,事实证明,使用 16 台攻击机破解 Erlang 服务器是不可能的 - 尽管很容易阻止 Apache 服务器。【参考方案6】:

因为 Erlang 解释器只需要担心自己,操作系统还有很多其他的事情要担心。

【讨论】:

【参考方案7】:

其中一个原因是erlang进程不是在操作系统中创建的,而是在evm(erlang虚拟机)中创建的,所以成本更小。

【讨论】:

以上是关于从技术上讲,为啥 Erlang 中的进程比 OS 线程更高效?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 OCaml 模式匹配比 Erlang 弱?

为啥我必须 sudo 来自守护进程的命令?

如果能够并行执行很少的进程,那么能够有效地生成许多进程有什么意义呢?

为啥第二次调用接收没有在 Erlang shell 中检索消息?

从技术上讲,啥是数据库连接?

如何跟踪erlang中的子进程?