发布于 2026-01-06 5 阅读
0

Erlang 虚拟机中的并发

Erlang 虚拟机中的并发

Erlang通常被称为“面向并发的编程语言”。

它为何得名?一种诞生于上世纪80年代、专为电信行业而创建的语言,如今又能如何帮助我们?

Erlang 或Erlang/OTP(开放电信平台)于 1986 年由爱立信公司开发,旨在应对日益增长的电话用户群。它的设计目标是改进电话应用的开发。它非常适合构建分布式、容错且高可用的系统。

那么,一项诞生于 80 年代的技术对我们有什么意义呢?事实证明,Erlang 最初的设计理念在今天对 Web 服务器仍然极其有用。我们需要应用程序快速可靠——而且我们希望它能立即实现!幸运的是,Phoenix 可以做到这一点,它是一个基于 Elixir(一种基于 Erlang 的现代语言)构建的 Web 框架。它拥有热代码切换和实时视图等强大功能——而且无需 JavaScript!

这一切是如何实现的?这很大程度上要归功于 Erlang 语言自下而上构建的并发特性——这要归功于 Erlang 虚拟机 BEAM。

在继续讲解之前,让我们回顾一下计算机科学入门课程(CS 101)中的一些概念——操作系统!

并发性

为了理解 Erlang VM 中的并发性为何如此特殊,我们需要了解操作系统是如何处理指令/程序的。

CPU负责机器的运行,其唯一职责是执行进程。进程是一个独立的执行块,拥有自己的内存、上下文和文件描述符。一个进程可以由多个线程组成,或者用一个略显老套的定义来说,就是“轻量级进程”。

操作系统进程和线程

CPU负责以最高效的方式执行这些进程。它可以按顺序执行进程,这被称为“顺序执行”。这是最基本、最古老的执行方式。但如今它对我们来说几乎毫无用处——就连我们现在使用的冰箱都能做得更好!

顺序处理

你可能会认为,既然现代电脑允许你同时处理多项任务,那么CPU就是在并行执行进程。就像这样:

平行结构

然而,这与事实相去甚远。我们距离真正的并行计算还很遥远。当我们尝试走这条路时,会涌现出一大堆问题。

其次是并发。并发执行意味着将进程拆分成多个小块,并在执行过程中进行切换。这个过程发生得非常快,以至于让用户感觉多个进程同时在运行——同时巧妙地规避了并行性带来的问题。

它看起来大概是这样的,

并发性

你可以看到CPU在进程1和进程2之间切换。这被称为上下文切换。这是一个相当耗费资源的任务,因为CPU需要在切换前将进程的所有信息和状态存储到内存中,然后在需要再次执行时再将其加载回来。然而,我们已经在这个问题上投入了多年的研究,因此我们在这方面已经非常熟练了。

最后还有一件事……

程序执行速度取决于CPU时钟周期。处理器速度越快,电脑运行速度就越快。摩尔定律指出,价格适中的CPU上的晶体管数量每两年翻一番。然而,如果你仔细观察,就会发现处理器速度在过去几年里并没有显著提升。这是因为我们遇到了物理瓶颈。当我们意识到将频率提升到远高于4GHz非常困难且徒劳时,这种情况就发生了。光速实际上成为了瓶颈!正因如此,我们决定采用多核处理。我们选择了横向扩展,而不是纵向扩展。我们增加了核心数量,而不是提升单个核心的性能。

好的,我知道我们已经讲了很多内容。但你需要记住的是,上下文切换开销很大,而且我们现在已经开始讨论多核处理了。接下来,让我们看看 Erlang 有哪些技巧使其成为一种面向并发的语言。

并发模型

在上一节中,我们得出结论:并发是实现多进程的最佳途径。多年来,各种编程语言都引入了不同的算法来实现并发。接下来,我们将介绍三种与我们讨论主题最相关的算法。

1. 共享内存模型

这是Java和C#等流行编程语言中常用的一种模型。它允许不同的进程访问同一块内存来进行存储和通信,从而降低上下文切换的开销。

虽然理论上听起来很棒,但当两个或多个进程尝试访问同一共享内存块时,就会出现一些棘手的问题,例如死锁。为了解决这个问题,我们使用了互斥锁、锁和同步机制。然而,这使得系统更加复杂,难以扩展。

共享内存

2. 演员模型

这是 Erlang 和 Rust 用于实现并发的模型。它依赖于尽可能地隔离进程,并将进程间的通信简化为消息传递。每个进程被称为一个 Actor。

参与者之间通过发送消息进行通信。消息可以随时发送,且不会阻塞。这些消息随后会存储在接收参与者的邮箱中。要读取消息,参与者必须执行阻塞式读取操作。

演员模型

3. 沟通顺序过程

这是 Go 语言使用的高效并发模型。它与 Actor 模型类似,都使用消息传递。然而,与 Actor 模型中仅接收消息是阻塞的不同,CSP 要求两个 action 都必须是阻塞的。

在CSP中,使用一个名为通道的独立存储来传输传入和传出的消息。进程之间不直接通信,而是通过通道层进行通信。

CSP

演员模型

让我们深入了解一下演员模型。

Actor 是一种原始的并发机制。每个 Actor 都有一个身份标识,其他 Actor 可以使用该标识来发送和接收消息。当收到消息时,消息会被存储在 Actor 的邮箱中。Actor 必须显式地尝试读取邮箱的内容。每个 Actor 都与其他 Actor 完全隔离,彼此之间不共享任何内存。这完全消除了对复杂同步机制或锁的需求。

演员收到消息后可以做三件事:

  1. 创造更多演员
  2. 向其他演员发送消息
  3. 指定如何处理下一条消息

前两个都很简单明了。我们来看看它在代码层面的表示。

Elixir代码

您可以看到,在 Elixir 中创建新进程正是如此。与其他使用多线程处理并发的语言不同,Elixir 使用的是线程。

需要记住的关键一点是,这些是 Erlang 线程,它们比操作系统线程轻量得多。平均而言,Erlang 进程比操作系统进程轻2000 倍。一个系统中即使有数十万个 Erlang 进程,也不会出现任何问题。

Erlang 线程也拥有一个进程 ID (PID)。通过 PID,您可以获取有关线程占用的内存、正在运行的函数以及更多其他信息。此外,您还可以使用 PID 向进程发送消息并进行通信。

让我们来看看演员的最终职责——指定角色。

Elixir代码

您可以看到,每个进程都有自己的内部状态。每次我们向进程发送消息时,它不会使用默认值重新启动函数,而是能够保留正在运行的进程的状态信息。本质上,每个参与者都能访问自己的内存空间并对其进行跟踪。

这三个特性使得 Erlang 进程非常轻量级。它无需复杂的同步技术,并且进程间通信也不会占用过多内存空间。所有这些都有助于实现高可用性。

此时此刻,观看这段关于演员模型的4分钟短视频是个绝佳的主意。毕竟,一张图片胜过千言万语,而一段视频更是如此!如果您有时间并且想要了解更多细节,不妨看看这段视频,演员模型的创建者在其中以非常精彩的方式解释了我刚才所说的一切。

光束

经过这么多话题,我们终于来到了真正的 Erlang 虚拟机——BEAM!

把我传送上去,斯科蒂

BEAM虚拟机旨在将 Erlang/Elixir 文件编译成字节码 (.beam) 文件,并能在 CPU 上调度 Erlang 进程。这种控制能力为高效并发运行进程提供了巨大优势。

虚拟机启动时,第一步是启动调度器。调度器负责在 CPU 上并发运行每个 Erlang 进程。由于所有进程都由 BEAM 维护和调度,因此我们可以更好地控制进程故障时的处理方式(容错性),并能更有效地利用内存,实现更流畅的上下文切换。

BEAM 还充分利用了硬件。它为每个可用核心启动一个调度器,使进程能够自由高效地运行,同时还能彼此通信。

BEAM VM

BEAM 允许 Erlang 创建数千个进程并实现高并发性。HackerNews 上有一篇精彩的讨论,探讨了数千个进程如何带来高效并发。

HN片段

阅读

促成因素

保持冷静,我们快要到达终点了!

Erlang 的并发性主要归功于 Actor 模型和 BEAM。然而,还有许多其他机制也确保了稳定性。我将提供一些简要的线索,供您进一步阅读。

1. 主管

Erlang 中有一个名为 Supervisor 的特殊进程。它的作用是判断进程失败时应该如何处理。它会帮助失败的进程重置为初始值,以便再次被送去处理。

2. 容错性

Erlang 语言的(已故)创始人乔·阿姆斯特朗曾说过一句名言:“让它崩溃吧。” 他设计这种容错语言的目的并非为了防止错误发生,而是为了构建能够处理各种错误场景的机制。

HN片段

整个帖子都非常感人,值得一读

3. 分配

最后,Erlang 使得构建分布式系统变得极其简单。每个 Erlang 实例都可以作为不同设备上的节点,并像创建进程一样轻松地进行通信。

4. 没有 GIL!

与其他解释型语言(最著名的当属 Ruby 和 Python)不同,Erlang 无需担心全局解释器锁 (GIL)。GIL 确保每个进程在 CPU 上只能运行一个 Ruby/Python 线程。这对并发性来说是一个巨大的打击。

像 Passenger 这样的应用服务器试图通过创建多个操作系统处理器并在多个核心上运行来解决这个问题。然而,正如我们之前看到的,管理操作系统线程的成本很高。

结论

Erlang 是一种结构优美、体系完善的语言。它经受住了时间的考验,并且在当今时代更具现实意义。

文章来源:https://dev.to/imswaathik/concurrency-in-the-erlang-vm-272k