演员模特:为什么erlang特别?或者,为什么你需要另一种语言呢?

Jon*_*nks 74 c++ concurrency erlang message-passing actor

我一直在研究学习erlang,结果一直在阅读(好的,略读)演员模型.

根据我的理解,actor模型只是一组函数(在erlang中称为"processes"的轻量级线程中运行),它们只通过消息传递相互通信.

在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;
}
Run Code Online (Sandbox Code Playgroud)

每个进程都是派生的BaseActor的一个实例.演员只能通过消息传递相互通信.(即推).Actors在初始化时使用中心映射注册自己,允许其他actor找到它们,并允许中心函数运行它们.

现在,我明白我错过了,或者更确切地说,在这里掩盖了一个重要的问题,即:缺乏屈服意味着单个演员可以不公平地消耗过多的时间.但是跨平台协程是否会使C++难以实现?(例如,Windows有光纤.)

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

我绝对不是想在这里开始一场火焰战争,我只想了解我所缺少的东西,因为这基本上就是我已经做过的能够在某种程度上推断并发代码的原因.

Luk*_*kas 83

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

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

此外,波束SMP仿真器还可以对参与者进行JIT调度,将它们移动到核心,此时核心是利用率最低的核心,如果不再需要,还可以休眠某些核心上的线程.

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

这些事情在C++中并非不可能,但如果你添加Erlang几乎适用于所有主要的hw和os配置的事实,它们会变得越来越难.

编辑:刚刚找到了Ulf Wiger关于他所看到的erlang风格并发性的描述.

  • 您列出的所有属性都由操作系统提供给进程.与其他任何程序一样,C++程序可以轻松地使用它们.我认为Erlang的关键在于它的演员比提供这些属性的OS流程便宜得多.因此,演员可以更自由地使用. (5认同)
  • @Karmastan是的,Erlang进程非常便宜,因为/并发性是结构化应用程序的基本抽象.我们更喜欢称他们为流程而不是演员,我们在设计Erlang时没有听说过演员.:-) (3认同)

rvi*_*ing 30

我不喜欢引用自己,而是来自Virding的第一条编程规则

在另一种语言中任何足够复杂的并发程序都包含一个特殊的非正式指定的错误驱动的Erlang一半的慢速实现.

关于格林斯普恩.乔(阿姆斯特朗)也有类似的规则.

问题不在于实施行动者,这并不困难.问题是让一切工作在一起:进程,通信,垃圾收集,语言原语,错误处理等等......例如,使用OS线程严重缩放,因此您需要自己完成.这就像试图"出售"一种OO语言,你只能拥有1k个对象,并且它们很难创建和使用.从我们的角度来看,并发性是构造应用程序的基本抽象.

走开,所以我会在这里停下来.


小智 21

这实际上是一个很好的问题,并且得到了很好的答案,这些答案可能还不令人信服.

为了增加阴影和强调已经在这里的其他伟大的答案,考虑Erlang 带走了什么(与传统的通用语言,如C/C++相比),以实现容错和正常运行时间.

首先,它夺走了锁.Joe Armstrong的书列出了这个思想实验:假设你的进程获得一个锁,然后立即崩溃(内存故障导致进程崩溃,或者电源无法进入系统的一部分).下一次进程等待同一个锁时,系统刚刚死锁.这可能是一个明显的锁,如示例代码中的AquireScopedLock()调用; 或者它可能是内存管理器代表您获取的隐式锁,比如在调用malloc()或free()时.

无论如何,您的进程崩溃现在已经阻止整个系统取得进展.菲尼.故事结局.你的系统已经死了.除非您能保证您在C/C++中使用的每个库都不会调用malloc并且从不获取锁,否则您的系统不具有容错能力.Erlang系统可以并且在重负载下随意杀死进程以便取得进展,因此在规模上你的Erlang进程必须是可用的(在任何单个执行点)才能保持吞吐量.

有一个部分解决方法:在任何地方使用租约而不是锁,但您不能保证您使用的所有库也都这样做.关于正确性的逻辑和推理很快就会变得毛茸茸.此外,租约恢复缓慢(超时到期后),因此整个系统在失败时变得非常缓慢.

其次,Erlang会消除静态类型,从而实现热代码交换并同时运行相同代码的两个版本.这意味着您可以在运行时升级代码而无需停止系统.这就是系统如何保持九个9或32毫秒的停机时间/年.它们只是升级到位.您的C++函数必须手动重新链接才能升级,并且不支持同时运行两个版本.代码升级需要系统停机,如果您的大型集群不能同时运行多个版本的代码,则需要立即关闭整个集群.哎哟.而在电信领域,不能容忍.

另外Erlang带走共享内存和共享共享垃圾回收; 每个轻量级过程都是独立的垃圾收集.这是第一点的简单扩展,但强调对于真正的容错,您需要在依赖性方面不互锁的进程.这意味着对于大型系统而言,与Java相比,GC暂停是可以容忍的(小而不是暂停半小时以完成8GB GC).