79

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

据我了解,actor 模型只是一组函数(在 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 找到它们,并允许中央功能通过它们运行。

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

不过,我还有什么遗漏的吗,或者模型真的这么明显吗?

4

6 回答 6

87

The C++ code does not deal with fairness, isolation, fault detection or distribution which are all things which Erlang brings as part of its actor model.

  • No actor is allowed to starve any other actor (fairness)
  • If one actor crashes, it should only affect that actor (isolation)
  • If one actor crashes, other actors should be able to detect and react to that crash (fault detection)
  • Actors should be able to communicate over a network as if they were on the same machine (distribution)

Also the beam SMP emulator brings JIT scheduling of the actors, moving them to the core which is at the moment the one with least utilization and also hibernates the threads on certain cores if they are no longer needed.

In addition all the libraries and tools written in Erlang can assume that this is the way the world works and be designed accordingly.

These things are not impossible to do in C++, but they get increasingly hard if you add the fact that Erlang works on almost all of the major hw and os configurations.

edit: Just found a description by Ulf Wiger about what he sees erlang style concurrency as.

于 2011-11-12T22:52:44.330 回答
32

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

任何用另一种语言编写的足够复杂的并发程序都包含一个临时的、非正式地指定的、充满错误的缓慢实现,它是 Erlang 的一半。

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

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

得意忘形,所以我会在这里停下来。

于 2011-11-13T21:59:25.137 回答
22

这实际上是一个很好的问题,并且得到了很好的答案,但可能还没有说服力。

为了给已经在这里的其他很好的答案添加阴影和强调,请考虑 Erlang为实现容错和正常运行时间而采取的措施(与 C/C++ 等传统通用语言相比)。

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

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

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

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

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

于 2012-11-21T18:29:38.237 回答
14

C++ 有实际的演员库:

以及其他语言的一些库的列表。

于 2011-11-13T07:51:09.363 回答
3

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

所有这一切意味着虽然你当然可以用另一种语言编写“演员模型”程序套件(我知道,我已经在 Python、C 和 Guile 中这样做了很长时间,但在遇到 Erlang 之前没有意识到这一点,包括一种形式的监视器和链接,在我听说过“演员模型”一词之前),了解您的代码实际产生的过程以及它们之间发生了什么非常困难。Erlang 执行的规则是操作系统在没有重大内核大修的情况下根本无法做到的——内核大修可能总体上不会有益。这些规则表现为对程序员的一般限制(如果你真的需要,可以随时绕过)和系统为程序员保证的基本承诺(如果你真的需要也可以故意破坏)。

例如,它强制两个进程不能共享状态以保护您免受副作用。这并不意味着每个函数都必须是“纯粹的”,因为一切都是引用透明的(显然不是,尽管使您的程序尽可能多地引用透明是大多数 Erlang 项目的明确设计目标),而是两个进程不会不断地创建与共享状态或争用相关的竞争条件。(顺便说一下,在 Erlang 的上下文中,这更多的是“副作用”的含义;与 Haskell 或玩具“纯”语言相比,知道这可能有助于您破译一些关于 Erlang 是否“真正实用”的讨论.)

另一方面,Erlang 运行时保证消息的传递。在您必须完全通过非托管端口、管道、共享内存和公共文件进行通信的环境中,这是非常错过的​​,而操作系统内核是唯一管理的(与 Erlang 相比,这些资源的操作系统内核管理必然非常少)运行时提供)。这并不意味着 Erlang 保证 RPC(无论如何,消息传递不是RPC,也不是方法调用!),它不保证您的消息被正确处理,也不保证您是一个进程尝试发送消息以存在或活着。如果您发送的东西在那个时候恰好是有效的,它只是保证交付。

建立在这个承诺之上的是监控和链接是准确的承诺。并且基于此,一旦你掌握了系统正在发生的事情(以及如何使用 erl_connect...),Erlang 运行时就会使“网络集群”的整个概念消失。这使您可以跳过一组棘手的并发案例,这使您在编写成功案例的代码方面有了很大的领先优势,而不是陷入裸并发编程所需的防御技术的沼泽中。

所以它并不是真的需要Erlang,这门语言,它是关于已经存在的运行时和 OTP,以一种相当干净的方式表达,并且用另一种语言实现任何接近它的东西都非常困难。OTP 只是一个很难遵循的行为。同样,我们也不需要C++,我们可以只使用原始二进制输入、Brainfuck 并将 Assembler 视为我们的高级语言。我们也不需要火车或轮船,因为我们都知道如何走路和游泳。

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

于 2014-09-02T00:35:21.327 回答
2

卡萨布兰卡是演员模特街区的另一个新人。典型的异步接受如下所示:

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

于 2012-05-01T05:49:33.833 回答