Erlang 进程与 Java 线程

Erlang Processes vs Java Threads

我正在阅读 "Elixir in Action" book by Saša Jurić, and in the first chapter 它说:

Erlang processes are completely isolated from each other. They share no memory, and a crash of one process doesn’t cause a crash of other processes.

Java 线程不也是如此吗?我的意思是当 Java 线程崩溃时,它也不会导致其他线程崩溃 - 特别是,如果我们正在查看请求处理线程(让 main 线程从这个讨论中排除)

Java 线程实际上可以共享内存。例如,您可以将同一个实例向下传递给两个单独的线程,并且两个线程都可以操纵其状态,从而导致潜在的问题,例如 deadlocks.

Elixir/Erlang 另一方面通过不变性的概念来解决这个问题,因此当您将某些内容传递给进程时,它将是原始值的副本。

绝对不是。 Java 中的所有线程共享相同的地址 space,因此一个线程可能会破坏另一个线程拥有的东西。在 Erlang VM 中这是不可能的,因为每个进程都与其他进程隔离。这就是他们的重点。任何时候你想让一个进程对来自另一个进程的数据做一些事情,你的代码必须向另一个进程发送一条消息。进程之间共享的唯一东西是大型二进制对象,它们是不可变的。

when Java thread dies, it too does not impact other threads

让我反问一下:为什么您认为 Thread.stop() 已经弃用了十多年?之所以如此,恰恰是对你上述说法的否定。

举两个具体的例子:你 stop() 一个线程正在执行一些听起来无害的东西,如 System.out.println()Math.random()。结果:这两个功能现在在整个 JVM 中都被破坏了。这同样适用于您的应用程序可能执行的任何其他同步代码。

if we are looking at request-processing threads

应用程序理论上可以编码,这样绝对不会使用任何受锁保护的共享资源;然而,这只会有助于指出 Java 线程相互依赖的确切程度。实现的 "independence" 仅适用于请求处理线程,而不适用于此类应用程序中的 所有 线程。

跟着我重复:“这些是不同的范例”

大声说 20 遍左右——这是我们目前的口头禅。

如果我们真的必须比较苹果和橘子,让我们至少考虑一下“是水果”的共同方面在哪里相交。

Java “对象”是 Java 程序员的基本计算单位。也就是说,object(基本上是一个带有封装的手臂和腿的结构 somewhat more strictly enforced than in C++)是您为世界建模的主要工具。您认为“这个对象 knows/has Data {X,Y,Z} 并对其执行 Functions {A(),B(),C()},随处携带 Data,并且可以通过调用定义的 functions/methods 与其他对象通信作为他们 public 界面的一部分。它是一个名词,那个名词 东西。”。也就是说,您围绕这些计算单元来定位您的思维过程。默认情况是对象之间发生的事情按顺序发生,并且崩溃会中断该顺序。它们被称为“对象”,因此(如果我们忽略 Alan Kay 的原意)我们得到“面向对象”。

Erlang“进程”是 Erlang 程序员的基本计算单位。 process(基本上是一个自包含的顺序程序 运行ning 在自己的时间和 space)是 Erlanger 用来模拟世界的主要工具(1 ).类似于 Java 对象定义封装级别的方式,Erlang 进程也定义封装级别,但在 Erlang 的情况下,计算单位 完全 从一个其他。您不能在另一个进程上调用方法或函数,也不能访问其中的任何数据,甚至 运行 一个进程也不能在与任何其他进程相同的时间上下文中,并且不能保证顺序相对于可能正在发送消息的其他进程的消息接收。他们也可能完全在不同的星球上(而且,仔细想想,这实际上是有道理的)。它们可以彼此独立地崩溃,并且其他进程只有在它们故意选择受到影响时才会受到影响(甚至这涉及消息传递:本质上是注册以接收来自死进程的遗书,该遗书本身不能保证以任何形式到达相对于整个系统的秩序,您可能会或可能不会选择做出反应)。

Java 直接处理复合算法中的复杂性:对象如何协同工作以解决问题。它旨在在单个执行上下文中执行此操作,Java 中的默认情况是顺序执行。 Java 中的多个线程表示多个 运行ning 上下文并且是一个非常复杂的主题,因为 activity 在不同的时间上下文中相互影响(以及整个系统:因此是防御性的)编程、异常方案等)。在 Java 中说“多线程”意味着 与它在 Erlang 中的不同,事实上这在 Erlang 中从来没有说过,因为它始终是基本情况。请注意,Java 线程意味着与时间相关的隔离,而不是内存或可见引用——Java 中的可见性是通过选择什么是私有的什么是 public 来手动控制的;系统的普遍可访问元素必须设计为“线程安全”和可重入,通过排队机制顺序化,或采用锁定机制。简而言之:调度是 threaded/concurrent Java 程序中的手动管理问题。

Erlang 在执行时间(调度)、内存访问和引用可见性方面将每个进程的 运行ning 上下文分开,这样做通过完全隔离算法来简化算法的每个组件 。这不仅仅是默认情况,这是该计算模型下唯一可用的情况。这是以永远不知道任何给定操作的确切顺序为代价的,一旦您的处理序列的一部分跨越了消息屏障——因为消息本质上都是网络协议,并且没有方法调用可以保证在给定的范围内执行语境。这类似于为每个对象创建一个 JVM 实例,并且只允许它们通过套接字进行通信——这在 Java 中会非常麻烦,但这是 Erlang 设计的工作方式(顺便说一句,这也是编写“Java 微服务”概念的基础,如果一个人抛弃流行语往往带来的面向 Web 的包袱——默认情况下,Erlang 程序是微服务群)。一切都与权衡有关。

这些是不同的范例。我们能找到的最接近的共同点是,从程序员的角度来看,Erlang 进程类似于 Java 对象。如果我们必须找到一些东西来比较 Java 线程......好吧,我们根本不会在 Erlang 中找到类似的东西,因为在 Erlang 中没有这样的可比较概念。打败死马:这些是不同的范例。如果您用 Erlang 编写一些重要的程序,这将变得显而易见。

请注意,我说的是“这些是不同的范例”,但我什至没有触及 OOP 与 FP 的话题。 “用 Java 思考”和“用 Erlang 思考”之间的区别比 OOP 与 FP 的区别更为根本。 (事实上​​ ,可以为 Erlang VM 编写一种 OOP 语言,其工作方式类似于 Java —— 例如:An implementation of OOP objects in Erlang。)

虽然 Erlang 的“面向并发”或“面向过程”基础确实更接近 Alan Kay 在创造“面向对象”(2) 术语时的想法,但这并不是这里的重点. Kay 的意思是,可以通过将您的计算机切割成离散的块来降低系统的认知复杂性,而为此隔离是必要的。 Java 以一种在本质上仍然是程序性的方式实现这一点,但围绕一种特殊语法构建代码,而不是称为“class 定义”的高阶调度闭包。 Erlang 通过为每个对象拆分 运行ning 上下文来做到这一点。这意味着 Erlang 事物不能相互调用方法,但是 Java 事物可以。这意味着 Erlang thingies 可以孤立地崩溃,但 Java thingies 不能。这种基本差异产生了大量的影响——因此产生了“不同的范式”。权衡。


脚注:

  1. 顺便说一下,Erlang 实现了“the actor model", but we don't use this terminology as Erlang predates the popularization of this model. Joe was unaware of it when he designed Erlang and wrote his thesis.
  2. 的一个版本
  3. Alan Kay 对 what he meant when he coined the term "object oriented", the most interesting being his take 消息传递(从一个具有自己的时间和内存的独立进程到另一个进程的单向通知)VS 调用(顺序执行上下文中的函数或方法调用)说了很多共享内存)——以及编程语言呈现的编程接口和底层实现之间的界线是如何模糊的。

为了补充前面的答案,Java 线程有两种类型:守护进程和非守护进程。

要更改线程的类型,您可以调用 .setDaemon(boolean on)。不同之处在于守护线程不会阻止 JVM 退出。正如 Thread 的 Javadoc 所说:

The Java Virtual Machine exits when the only threads running are all daemon threads.

这意味着:用户线程(那些没有专门设置为守护进程的线程)防止 JVM 终止。另一方面,当所有非守护线程都完成时,守护线程可能 运行,在这种情况下 JVM 将退出。因此,回答您的问题:您可以启动一个线程,该线程在完成时不会退出 JVM。

关于与 Erlang/Elixir 的比较,请不要忘记:它们是不同的范例,如前所述。

JVM 并非不可能模仿 Erlang 的行为,尽管它并非出于预期目的,因此,它需要进行很多权衡。以下项目试图实现这一目标:

Isn't that true for Java threads as well? I mean when Java thread crashes, it too does not crash other threads

是和不是。我解释一下:

  • 指的是共享内存:Java进程中的不同线程共享整个堆,因此线程可以以大量计划内和计划外的方式进行交互。 但是 堆栈中的对象(例如,您传递给被调用方法的上下文)或 ThreadLocal 是它们自己的线程(除非它们开始共享引用)。

  • 崩溃:如果一个线程在 Java 中崩溃(一个 Throwable 传播到 Thread.run(),或者某些东西被循环或阻塞),那么这个事故可能不会影响其他线程(例如,服务器中的连接池将继续运行)。 但是因为不同线程交互。如果其中一个线程异常结束,其他线程很容易陷入困境(例如,一个线程试图从另一个没有关闭其末端的线程读取空管道)。所以除非开发者高度偏执小心,否则很有可能会出现副作用

我怀疑任何其他范例都打算将线程作为完全独立的孤岛运行。他们必须共享信息并以某种方式进行协调。然后就有机会把事情搞砸了。只是他们会采取更具防御性的方法 "gives you less rope to hang yourself"(与指针相同的成语)。

一开始,诉求可能是这样的。在这样一个受限的对比环境中(“一个撞到另一个”),它们可能看起来是一样的。但当我们深入了解它们的细节和本质时,真正的区别就会浮出水面。 @zxq9 给出了相当多的对比细节,可能有助于理解它们确实在细节上有所不同。

-- 当谈到分布式系统时,ErLang 是一个工程奇迹。它对问题领域的看法确实是非凡的,而且它对系统资源的处理方式确实与众不同。在其他任何地方都不会遇到。