我一直在研究学习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;
}
每个进程都是派生的BaseActor的实例。演员只能通过消息传递相互通信。 (即推)。 Actors在初始化时使用中心映射注册自己,允许其他actor找到它们,并允许中心函数运行它们。
现在,我理解我错过了,或者更确切地说,在这里掩盖了一个重要问题,即: 缺乏屈服意味着单个演员可以不公平地消耗过多的时间。但是跨平台协程是否会使C ++难以实现? (例如,Windows有光纤。)
但是,我还缺少什么,或者模型真的很明显吗?
我绝对不想在这里开始一场火焰战争,我只是想了解我所缺少的东西,因为这基本上就是我已经做过的,能够对并发代码有所了解。
答案 0 :(得分:83)
C ++代码不涉及公平性,隔离,故障检测或分发,这些都是Erlang作为其actor模型的一部分所带来的。
此外,波束SMP仿真器还可以对参与者进行JIT调度,将它们移动到核心,此时核心是利用率最低的核心,如果不再需要,还可以休眠某些核心上的线程。
此外,用Erlang编写的所有库和工具都可以假设这是世界的工作方式并相应地进行设计。
这些事情在C ++中并非不可能,但如果你添加Erlang几乎适用于所有主要的hw和os配置的事实,它们会变得越来越难。
编辑:刚刚找到Ulf Wiger关于他所看到的erlang风格并发性的描述。
答案 1 :(得分:29)
我不想引用自己,而是来自Virding's First Rule of Programming
另一种语言中任何足够复杂的并发程序都包含一个特殊的非正式指定错误驱动的Erlang一半的慢速实现。
关于格林斯普尔。乔(阿姆斯特朗)也有类似的规则。
问题不在于实施演员,这并不困难。问题是让一切工作在一起:进程,通信,垃圾收集,语言原语,错误处理等等......例如,使用OS线程严重缩放,因此您需要自己完成。这就像试图“出售”一种OO语言,你只能拥有1k个对象,并且它们很难创建和使用。从我们的角度来看,并发性是构建应用程序的基本抽象。
走开,所以我会在这里停下来。
答案 2 :(得分:21)
这实际上是一个很好的问题,并且得到了很好的答案,可能还不能令人信服。
为了增加阴影和强调已经在这里的其他好的答案,考虑Erlang 带走什么(与传统的通用语言,如C / C ++相比),以实现容错和正常运行时间
首先,它夺走了锁。 Joe Armstrong的书列出了这个思想实验:假设你的进程获得一个锁,然后立即崩溃(内存故障导致进程崩溃,或者电源无法进入系统的一部分)。下一次进程等待同一个锁时,系统刚刚死锁。这可能是一个明显的锁,如示例代码中的AquireScopedLock()调用;或者它可能是内存管理器代表您获取的隐式锁,比如在调用malloc()或free()时。
在任何情况下,您的进程崩溃现在都会阻止整个系统取得进展。菲尼。故事结局。你的系统已经死了。除非您能保证您在C / C ++中使用的每个库都不会调用malloc并且从不获取锁,否则您的系统不具有容错能力。 Erlang系统可以并且在重负载下随意杀死进程以便取得进展,因此在规模上你的Erlang进程必须是可用的(在任何单个执行点)以保持吞吐量。
有一个部分解决方法:在任何地方使用租约而不是锁,但是您不能保证您使用的所有库也都这样做。关于正确性的逻辑和推理很快就会变得毛茸茸。此外,租约缓慢恢复(超时到期后),因此整个系统在失败时变得非常缓慢。
其次,Erlang取消了静态类型,这反过来又可以实现热代码交换并同时运行相同代码的两个版本。这意味着您可以在运行时升级代码而无需停止系统。这就是系统如何保持九个9或32毫秒的停机时间/年。它们只是升级到位。您的C ++函数必须手动重新链接才能升级,并且不支持同时运行两个版本。代码升级需要系统停机,如果您的大型集群不能同时运行多个版本的代码,则需要立即关闭整个集群。哎哟。在电信领域,不能容忍。
另外,Erlang带走了共享内存和共享共享垃圾回收;每个轻量级过程都是独立的垃圾收集。这是第一点的简单扩展,但强调对于真正的容错,您需要在依赖性方面不互锁的进程。这意味着对于大型系统而言,与java相比,GC暂停是可以忍受的(小而不是暂停半小时以完成8GB GC)。
答案 3 :(得分:14)
C ++有实际的actor库:
答案 4 :(得分:3)
关于演员模型的要少得多,而且在C ++中正确编写类似于OTP的东西有多难。此外,不同的操作系统提供完全不同的调试和系统工具,Erlang的VM和几种语言结构支持统一的方式来确定所有这些过程是如何以统一的方式很难做到的跨越多个平台(或者可能会)。 (重要的是要记住,Erlang / OTP早于当前的术语&#34;演员模型&#34;,所以在某些情况下这些讨论是比较苹果和翼手龙;伟大的想法倾向于独立发明。)
所有这一切都意味着虽然你当然可以写一个&#34;演员模型&#34;另一种语言的程序套件(我知道,我在Python,C和Guile中已经很长时间没有意识到这一点,在我遇到Erlang之前没有意识到这一点,包括一种监视器和链接,在我听到之前术语&#34;演员模型&#34;),了解代码实际产生的过程以及中发生的事情是非常困难的。 Erlang强制执行操作系统根本无法进行重大内核检修的规则 - 内核检修可能不会对整体有益。这些规则表现为对程序员的一般限制(如果你真的需要,它总是可以得到)和系统为程序员保证的基本承诺(如果你真的需要也可以故意破坏)。
例如,它强制执行两个进程无法共享状态以保护您免受副作用。这并不意味着每个功能都必须是纯粹的&#34;从某种意义上说,所有内容都是引用透明的(显然不是这样,尽管使得你的程序中的实际上是透明的,实际上是大多数Erlang项目的明确设计目标),而是两个进程都没有。不断创造与共享状态或争用相关的竞争条件。 (这更像是&#34;副作用&#34;意味着在Erlang的上下文中,顺便说一下;知道这可以帮助你破译一些讨论质疑Erlang是否真的有功能&#34;与Haskell或玩具&#34;纯&#34;语言相比时。)
另一方面,Erlang运行时保证了消息的传递。在您必须纯粹通过非托管端口,管道,共享内存和公共文件进行通信的环境中,这是操作系统内核是唯一一个管理的环境(与Erlang相比,这些资源的操作系统内核管理必然极少)运行时提供)。这并不意味着Erlang保证RPC(无论如何,消息传递不是 RPC,也不是方法调用!),它不承诺你的消息被正确地解决,并且它并不承诺您尝试发送消息的进程存在或存在。如果你发送的东西恰好在那一刻有效,它只保证交付。
建立在这一承诺基础上的是监控和链接准确的承诺。并且基于此,Erlang运行时创建了&#34;网络集群的整个概念。一旦掌握了系统发生的事情(以及如何使用erl_connect ......),就会融化掉。这允许你跳过一组棘手的并发案例,这使得人们在编写成功案例方面有了一个很大的开端,而不是陷入裸体并发编程所需的防御技术的沼泽中。
所以它并非真正关于需要 Erlang,语言,它关于运行时和OTP已经存在,以一种相当干净的方式表达,并用另一种语言实现任何接近它的东西都非常困难。 OTP只是一个很难遵循的行为。同样,我们也不需要 C ++,我们可以坚持使用原始二进制输入,Brainfuck并将Assembler视为我们的高级语言。我们也不需要火车或轮船,因为我们都知道如何散步和游泳。
所有这一切,VM的字节码都有详细记录,并且已经出现了许多替代语言,这些语言可以编译到它或与Erlang运行时一起使用。如果我们将问题分解为语言/语法部分(&#34;我是否必须了解Moon Runes进行并发?&#34;)和平台部分(&#34; OTP是最成熟的并发方式)并且它会引导我围绕在并发的分布式环境中发现的最棘手,最常见的陷阱吗?&#34;)然后答案是(&#34;不&#34;,&#34;是&#34; )。
答案 5 :(得分: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在将一个漂亮的界面隐藏在模式匹配后做得更好。)