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


77

我一直在研究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的实例。Actor仅通过消息传递相互通信。(即推动)。参与者在初始化时向中心地图注册自己,该地图允许其他参与者找到他们,并允许中心功能贯穿他们。

现在,我知道我在这里遗漏了,或者更确切地说,是在掩盖一个重要问题,即:缺乏屈服意味着单个Actor可能不公平地消耗过多时间。但是跨平台协程是C ++中使这一点变得困难的主要因素吗?(例如,Windows具有纤维。)

我还有什么想念的吗,或者模型真的很明显吗?


5
编程语言的目的是帮助表达思想或规格。actor模型在Erlang中是隐式的,因此尽管您可以用两种语言在模型中表达您的想法,但在Erlang中会更好,因为样板已为您完成。
GManNickG

3
@GMan一旦完成样板工作(我认为这将是一次尝试)有什么优势?
塞斯·卡内基

4
@SethCarnegie:这确实是我的问题的要点。
乔纳森·温克斯

15
erlang进程可以驻留在同一台机器上,也可以驻留在不同的物理机器上(为此编写的实际代码或多或少是相同的),因此您的示例似乎在简化。而且,关于热交换代码,C ++可以轻松做到这一点吗?您的c ++ actor内存是否已沙箱化?
凯文

20
如果您能够像使用erlang一样轻松地用C ++实现安全,可靠,并发和可维护的代码,那么请继续。有从这个片段缺失,但。erlang的核心是可靠性。如果某个进程无法执行其任务,则该进程将失败,并且该失败消息将通过系统传播,从而使复杂的依赖关系图能够根据各种中断(或错误)重新组织自身。您可以做到,但是您应该问为什么没人这样做。那就是导致新语言的原因。
达斯汀

Answers:


86

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

  • 不允许任何演员饿死任何其他演员(公平)
  • 如果一个actor崩溃,那么应该只影响那个actor(隔离)
  • 如果一个参与者崩溃,其他参与者应该能够检测到该崩溃并对之做出反应(故障检测)
  • Actor应该能够通过网络进行通信,就像他们在同一台计算机上一样(分发)

Beam SMP仿真器还带来了参与者的JIT调度,将他们移动到核心上,这是目前利用率最低的核心,并且如果不再需要它们,则可以休眠某些核心上的线程。

此外,用Erlang编写的所有库和工具都可以假定这是世界运作的方式,并进行了相应的设计。

这些事情在C ++中并非不可能完成,但是如果您添加一个事实,即Erlang可以在几乎所有主要的硬件和操作系统配置上运行,那么它们就会变得越来越困难。

编辑:刚刚找到了乌尔夫·维格Ulf Wiger)关于他将erlang样式并发视为的描述。


1
我肯定会在erlang并发模型中包括进程隔离错误处理,否则Ulf会写的很好。
rvirding 2011年

5
您列出的所有属性均由操作系统提供给进程。C ++程序可以像其他任何程序一样轻松地使用它们。我认为,Erlang的关键在于,它的参与者比提供这些属性的OS进程便宜得多。结果,演员可以更自由地使用。
arma斯坦2011年

3
@Karmastan是的,Erlang流程非常便宜,因为/并发是结构化应用程序的基本抽象。我们更喜欢称它们为过程而不是参与者,我们在设计Erlang时没有听说过参与者。:-)
rvirding

30

我不想引述自己,而是摘自Virding的《编程第一法则》

任何使用另一种语言的足够复杂的并发程序都包含一个临时的,非正式指定的,臭虫缠身的,缓慢的Erlang一半实现。

关于格林斯潘。乔(阿姆斯特朗)也有类似的规定。

问题不在于实现参与者,这不是那么困难。问题是要使所有东西协同工作:流程,通信,垃圾回收,语言原语,错误处理等……例如,使用OS线程的伸缩性很差,因此您需要自己做。这就像尝试“销售”一种OO语言,在这种语言中,您只能有1k个对象,并且创建和使用它们很繁琐。从我们的角度来看,并发是构建应用程序的基本抽象。

被迷住了,所以我会在这里停下来。


“所以我会在这里停下来”:还有更多有趣的事
serv-inc

22

这实际上是一个很好的问题,并且收到了也许还没有说服力的出色答案。

要在此处已经为其他出色的答案增加阴影和重点,请考虑Erlang所带走的功能(与传统的通用语言(例如C / C ++)相比),以实现容错和正常运行时间。

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

无论如何,您的进程崩溃现在已使整个系统停止前进。菲尼 故事结局。您的系统已死。除非您可以保证在C / C ++中使用的每个库都不会调用malloc且不会获取锁,否则您的系统就不会容错。Erlang系统可以在繁重的工作中随意杀死进程以取得进展,因此,为了保持吞吐量,您的Erlang进程必须是可杀死的(在任何单个执行点)。

有一个局部的解决方法:在各处使用租约而不是锁定,但是您不能保证所使用的所有库也都可以做到这一点。而且关于正确性的逻辑和推理很快就会变得很困难。此外,租约恢复缓慢(在超时到期后),因此面对故障,您的整个系统真的变慢了。

其次,Erlang取消了静态类型化,而静态类型化又启用了热代码交换并同时运行同一代码的两个版本。这意味着您可以在运行时升级代码而无需停止系统。这就是系统保持每年9个9或32毫秒的停机时间的方式。它们只是在原地升级。您的C ++函数必须手动重新链接才能升级,并且不支持同时运行两个版本。代码升级需要系统停机,并且,如果您有一个大型集群无法一次运行多个版本的代码,则需要立即关闭整个集群。哎哟。而且在电信界,是不能容忍的。

另外,Erlang删除了共享内存和共享共享垃圾回收;每个轻量级过程都是独立收集的垃圾。这是对第一点的简单扩展,但强调为了实现真正的容错能力,您需要的流程之间没有依赖关系。这意味着对于大型系统,与Java相比,您的GC暂停是可以容忍的(较小的而不是为8GB GC完成而暂停半小时)。


1
首先,您可以使用lock_guard,它会在程序崩溃时释放您的锁。其次,您可以用C ++实现热插拔系统,但是这很麻烦。并发问题是同步原语(甚至原子)会引入内存屏障和障碍,并减慢速度。您拥有的线程越多,您放慢的速度就越多。Erlang作为Clojure或Haskell,不使用互斥或​​原子,这迫使开发人员以不同的方式解决问题。这是解决并发问题的非常有效的方法
Asier Gutierrez 2015年

听起来很有效,但这只是C ++的比较,而C ++总是很容易受到指责。例如,是否有可能在Java(或Clojure)中使用它?Java中的锁是安全的,并且有多种方法可以在运行时编译/加载代码(在Clojure中,这也非常容易)。
显示名称


3

关于actor模型的内容要少得多,而要正确编写类似于C ++中的OTP的内容要花多大的力气,要多得多。同样,不同的操作系统提供了截然不同的调试和系统工具,Erlang的VM和多种语言构造支持一种统一的方式来弄清所有这些进程所要完成的工作,而这将很难以统一的方式完成(或者也许根本)。(重要的是要记住,Erlang / OTP早于对“演员模型”一词的热议,因此在某些情况下,这类讨论是在比较苹果和翼手龙;好主意容易产生独立发明。)

所有这些意味着,尽管您当然可以用另一种语言编写一套“角色模型”程序集(我知道,我在Python,C和Guile中已经做了很长时间,但在遇到Erlang之前就没有意识到这一点,其中包括监视器和链接,并且在我还没听说过“角色模型”一词之前,请先了解您的代码实际上是如何产生的,以及其中发生什么非常困难。Erlang执行的规则是,如果没有重大的内核大修,操作系统根本无法做-内核大修可能对整体没有好处。这些规则既体现为对程序员的一般限制(如果确实需要,可以总是解决),也可以是对程序员的系统保证的基本承诺(如果确实需要,可以故意打破)。

例如,它强制两个进程不能共享状态,以保护您免受副作用。这并不意味着每一个函数必须是“纯”,在这个意义上,一切都是引用透明(显然不是,虽然使尽可能多程序的引用透明实用是最二郎项目的一个明确的设计目标),而是两个流程并不是一直在创造与共享状态或竞争有关的竞争条件。(顺便说一下,(“副作用”在Erlang中更意味着什么;知道这可能有助于您解释一些讨论,这些问题是与Haskell或玩具“纯”语言相比,Erlang是否“确实具有功能”) )

另一方面,Erlang运行时保证消息的传递。在必须纯粹通过非托管端口,管道,共享内存和通用文件进行通信的环境中,这是一个非常遗憾的事情,而OS内核是唯一一个管理该对象的工具(与Erlang相比,对这些资源的OS内核管理必须非常少)运行时提供)。这并不意味着Erlang保证RPC(无论如何,消息传递不是RPC,也不是方法调用!),它不保证您的消息已正确处理,也不保证您正在使用的进程试图发送一条消息到存在或仍然存在。如果您发送到的东西在那一刻是有效的,它只是保证交货。

在此承诺的基础上,保证监视器和链接准确无误。并且基于此,一旦您掌握了系统的运行情况(以及如何使用erl_connect ...),Erlang运行时就会使“网络集群”的整个概念消失。这使您可以跳过一组棘手的并发案例,这为成功案例的编码提供了很大的开端,而不会陷入裸露的并行编程所需的防御技术的泥潭。

因此,它不是真正的需要二郎,语言,其对运行时和OTP已经存在,在一个相当干净的方式被表达,并执行任何接近它的另一种语言是非常难。OTP只是很难遵循的行为。同样,我们也不是真的需要C ++,我们可以坚持使用原始二进制输入,即Brainfuck,并考虑使用Assembler作为我们的高级语言。我们也不需要火车或轮船,因为我们都知道如何走路和游泳。

综上所述,VM的字节码已被详细记录,并且出现了许多可替代的语言,可对其进行编译或与Erlang运行时一起使用。如果我们将问题分解为语言/语法部分(“我必须了解Moon Runes来进行并发吗?”)和平台部分(“ OTP是进行并发的最成熟的方法,它会指导我解决最棘手的问题吗? ,最常见的陷阱是在并发的分布式环境中找到的?”,答案是(“否”,“是”)。


2

卡萨布兰卡(Casablanca)是演员模型块上的另一个新孩子。典型的异步接受如下所示:

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在将模式匹配隐藏在一个不错的界面后面方面做得更好。)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.