演员模特: 为什么 Erlang/OTP 很特别? 你能用另一种语言吗?

我一直在研究学习 Erlang/OTP,因此,我一直在阅读(好吧,略读)关于演员模型的资料。

据我所知,角色模型只是一组功能(在 Erlang/OTP 中称为“进程”的轻量级线程中运行) ,它们只通过 传递信息相互通信。

这在 C + + 或其他语言中实现起来似乎相当简单:

class BaseActor {
std::queue<BaseMessage*> messages;
CriticalSection messagecs;
BaseMessage* Pop();
public:
void Push(BaseMessage* message)
{
auto scopedlock = messagecs.AquireScopedLock();
messagecs.push(message);
}
virtual void ActorFn() = 0;
virtual ~BaseActor() {} = 0;
}

每个进程都是派生的 BaseActor 的实例。参与者之间只通过消息传递进行交流。(即推)。角色使用初始化时的中心映射注册自己,该映射允许其他角色找到它们,并允许一个中心函数通过它们运行。

现在,我知道我忽略了,或者更确切地说,我忽略了一个重要的问题,那就是: 缺乏让步意味着一个演员可以不公平地消耗过多的时间。但是,在 C + + 中,跨平台协同程序是否是造成这种困难的主要原因呢?(例如,窗户就有纤维。)

我还漏掉了什么吗,还是模型真的这么明显?

13759 次浏览

C + + 代码不处理公平性、隔离性、故障检测或分发,这些都是 Erlang 作为其参与者模型的一部分带来的。

  • 任何演员都不允许饿着其他演员(公平)
  • 如果一个参与者崩溃,它应该只影响该参与者(隔离)
  • 如果一个参与者崩溃,其他参与者应该能够检测并对该崩溃作出反应(故障检测)
  • 参与者应该能够通过网络进行通信,就好像他们在同一台机器(分发版)上一样

此外,梁 SMP 模拟器带来了参与者的 JIT 调度,将它们移动到目前利用率最低的核心,并且如果不再需要它们,还会冬眠某些核心上的线程。

此外,所有在 Erlang 编写的图书馆和工具都可以假定这就是世界运作的方式,并据此进行设计。

这些事情在 c + + 中并不是不可能做到的,但是如果你添加一个事实,那就是 Erlang 可以处理几乎所有主要的 hw 和 os 配置,那么这些事情就会变得越来越困难。

Edit: 刚刚发现了 乌尔夫 · 威格关于他认为 erlang 样式并发是什么的描述。

我不喜欢引用我自己,但从 编程的第一原则

任何用另一种语言编写的足够复杂的并发程序都包含一个特别的、非正式指定的、充满 bug 的、缓慢实现 Erlang 一半地区的程序。

乔(阿姆斯特朗)也有类似的规则。

问题不在于执行参与者,这并不难。问题是要让所有的东西一起工作: 进程、通信、垃圾收集、语言原语、错误处理等等... ... 例如,使用 OS 线程的扩展性很差,所以你需要自己来做。这就像试图“销售”一种面向对象语言,在这种语言中,您只能拥有1k 个对象,而且这些对象的创建和使用都非常繁重。从我们的角度来看,并发是构造应用程序的基本抽象。

我太激动了,所以就到这里吧。

卡萨布兰卡 是角色模型块中的另一个新孩子,典型的异步接受看起来像这样:

PID replyTo;
NameQuery request;
accept_request().then([=](std::tuple<NameQuery,PID> request)
{
if (std::get<0>(request) == FirstName)
std::get<1>(request).send("Niklas");
else
std::get<1>(request).send("Gustafsson");
}

(就我个人而言,我发现 CAF在把模式匹配隐藏在一个漂亮的界面后面方面做得更好。)

这实际上是一个很好的问题,而且已经得到了很好的回答,可能还不能令人信服。

为了给这里已有的答案加上阴影和强调,考虑一下 Erlang 带走(与传统的通用语言如 C/C + + 相比) ,以实现容错和正常运行时间。

首先,它可以拿走锁。乔 · 阿姆斯特朗的书展示了这个思想实验: 假设你的进程获得了一个锁,然后立即崩溃(一个内存故障导致进程崩溃,或者系统的一部分断电)。下一次进程等待同一个锁时,系统就死锁了。这可能是一个明显的锁,如示例代码中的 AquireScopedLock ()调用; 也可能是内存管理器代表您获取的隐式锁,比如在调用 malloc ()或 free ()时。

无论如何,您的进程崩溃现在已经阻止了整个系统取得进展。结束了。就这样。你的系统死机了。除非您能够保证您在 C/C + + 中使用的每个库永远不会调用 malloc 并且永远不会获得锁,否则您的系统是不能容错的。Erlang 系统可以在重负载下随意杀死进程以取得进展,因此在规模上,为了维持吞吐量,Erlang 进程必须是可杀死的(在任何单一执行点)。

有一个部分解决方案: 在任何地方都使用租约而不是锁,但是您不能保证您所使用的所有库也这样做。关于正确性的逻辑和推理很快就变得非常棘手。此外,租约恢复缓慢(超时过期后) ,所以整个系统在面临故障时变得非常缓慢。

其次,Erlang 去掉了静态类型,这反过来又支持热代码交换和同时运行同一代码的两个版本。这意味着您可以在运行时升级代码,而无需停止系统。这就是系统如何在每年9秒或32毫秒的停机时间内保持运行。它们只是在适当的位置进行了升级。您的 C + + 函数必须手动重新链接才能升级,并且不支持同时运行两个版本。代码升级需要系统停机,如果有一个大型集群不能同时运行多个版本的代码,则需要同时关闭整个集群。哎哟。在电信行业,这是不可容忍的。

此外,Erlang 带走了共享内存和共享垃圾回收; 每个轻量级进程都是独立回收垃圾的。这是对第一点的简单扩展,但强调为了实现真正的容错,您需要不依赖于互锁的进程。这意味着与 java 相比,GC 暂停对于大型系统来说是可以忍受的(比8GB GC 完成需要半个小时的暂停要少)。

它更多的是关于在 C + + 中正确地编写一些类似于 OTP 的东西是多么困难,而不是关于角色模型。此外,不同的操作系统提供了完全不同的调试和系统工具,Erlang 的 VM 和几种语言结构支持一种统一的方式来确定所有这些进程到底在做什么,而这些进程很难在多个平台上以统一的方式(或者根本不做)完成。(重要的是要记住,Erlang/OTP 早于当前有关“演员模式”的讨论,因此在某些情况下,这类讨论是在比较苹果和翼手龙,伟大的想法往往是独立发明的。)

所有这一切意味着,尽管你当然可以用另一种语言编写一套“ Actor 模型”程序(我知道,在遇到 Erlang 之前,包括一种形式的监视器和链接之前,我已经在 Python、 C 和 Guile 中编写了很长时间,没有意识到这一点,也没有听说过“ Actor 模型”这个术语) ,但是理解你的代码实际上是如何产生的进程以及发生了什么是极其困难的。Erlang 强制执行了操作系统不能没有重大内核检查的规则——内核检查可能不会有什么好处。这些规则表现为对程序员的一般限制(如果确实需要,总是可以绕过这些限制)和对程序员的系统保证的基本承诺(如果确实需要,也可以故意打破这些承诺)。

例如,它强制两个进程不能共享状态以保护您免受副作用的影响。这并不意味着每个函数都必须是“纯粹的”,即所有事情都是参照透明的(显然不是,尽管让程序的 差不多参照透明是大多数 Erlang 项目的明确设计目标) ,而是两个过程不会不断地创建与共享状态或争用相关的竞态条件。(顺便说一下,这更多的是“副作用”在 Erlang 语境中的意思,知道这一点可以帮助你解读一些关于 Erlang 与 Haskell 或玩具“纯”语言相比是否“真正有用”的讨论。)

另一方面,Erlang 运行时保证消息的传递。在这样一个环境中,你必须通过非托管端口、管道、共享内存和公共文件进行通信,而操作系统内核只管理这些文件(与 Erlang 运行时提供的相比,操作系统内核对这些资源的管理必然是极其简单的)。这并不意味着 Erlang 保证 RPC (无论如何,消息传递是 没有 RPC,也不是方法调用!),它不能保证您的消息被正确地处理,也不能保证您试图发送消息的进程存在或者是活的。它只是保证交货,如果你发送的东西碰巧是有效的那一刻。

建立在这个承诺之上的是监视器和链接是准确的承诺。在此基础上,一旦您掌握了系统中发生的事情(以及如何使用 erl _ connect...) ,Erlang 运行时就会让“网络集群”的整个概念消失。这使您可以跳过一组复杂的并发案例,从而在编写成功案例的代码方面有一个很好的开端,而不是陷入赤裸并发编程所需的防御技术的泥潭。

所以它不是真的关于 需要 Erlang 语言,它是关于运行时和 OTP 已经存在的,以一种相当干净的方式表达,并且用另一种语言实现任何类似的东西都是非常困难的。OTP 只是很难跟上。同样,我们也不是真正的 需要 c + + ,我们可以坚持使用原始的二进制输入,Brainfuck 语言,并把汇编语言作为我们的高级语言。我们也不需要火车或轮船,因为我们都知道如何行走和游泳。

尽管如此,虚拟机的字节码已经有了很好的文档说明,并且出现了许多可以编译到它或者与 Erlang 运行时一起工作的替代语言。如果我们把这个问题分解成一个语言/语法部分(“我必须了解月亮符文才能做并发吗?”)以及一个平台部分(“ OTP 是实现并发的最成熟的方法吗? 它会指导我在并发的分布式环境中找到最棘手、最常见的陷阱吗?”)那么答案就是(“不”,“是”)。