如何“成为”?用支持参与者模型的语言实现?

发布于 2024-09-10 22:03:15 字数 427 浏览 2 评论 0原文

Gul Agha 在他的技术报告“Actors:分布式系统中的并发计算模型”中很好地描述了 Actor 模型。

在第49页他解释了“成为”命令:

become <expression>

在调用“成为X”后,一个演员会将他所有的消息转发到另一个演员的邮箱(X)。

然而,我不确定这是如何在 Erlang 和 Scala 等语言中实现的(它根本就实现了)。我必须手动编码吗?那效率呢? Agha 展示了使用消息传递的堆栈实现。每次执行弹出或推送时,都会向某个参与者添加一个转发链接...经过数十万次操作后,我预计这样的实现会花费太多时间转发消息而不是执行实际工作,除非某些在幕后进行了很好的优化。

所以我的问题是:如何在 Erlang、Scala(以及其他语言的库)等典型 Actor 语言中实现转发(或“成为”)?

The actor model is nicely described by Gul Agha on his technical report, "Actors: a model of concurrent computation in distributed systems".

On page 49 he explains the "become" command:

become <expression>

After calling "become X", an actor will forward all his messages to another actor's mailbox (X).

I'm not sure, however, how this is implemented (it it's implemented at all) in languages like Erlang and Scala. Is it somethign I have to code manually? What about efficiency? Agha shows an implementation of a stack using message-passing. Every time a pop or push is performed, one more forwarding link is added to some actor... After hundreds of thousands of operations, I'd expect such an implementation to spend too much time forwarding messages and not doing actual work, unless some nice optimizations were performed under the hood.

So my question is: how is forwarding (or "become") implemented in typical actor languages like Erlang, Scala (and libraries for other languages)?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

原来是傀儡 2024-09-17 22:03:15

它没有直接在 Erlang 中实现,但您可以编写一个简单的 become 函数来接收消息,将其转发到另一个进程,然后调用自身:(

become(Pid) ->
    receive
        Msg -> Pid ! Msg
    end,
    become(Pid).

工业强度版本可能需要处理信号和其他零碎的东西,但这就是它的本质。)

调用 become(Pid) 将有效地将调用进程从外部世界转变为进程 Pid看法。

这并不能解决您强调的问题,重复调用 become 会导致转发链的增长。然而,这在 Erlang 中通常不会发生,而且我不确定 Erlang 的进程如何映射到 Actor 模型上。

It isn't directly implemented in Erlang, but you could write a trivial become function that receives a message, forwards it to another process and then calls itself:

become(Pid) ->
    receive
        Msg -> Pid ! Msg
    end,
    become(Pid).

(An industrial-strength version of this might need to deal with signals and other odds and ends, but this is the essence of it.)

Calling become(Pid) would effectively turn the calling process into process Pid from the outside world's perspective.

This doesn't address the problems you highlight, with repeated calls to become causing growth of forwarding chains. However, this doesn't generally occur in Erlang, and I'm not sure how Erlang's processes map onto the Actor model.

甚是思念 2024-09-17 22:03:15

Actor 是一个逆变协函子,因此“成为”就是 comap。

换句话说,T 类型消息上的 Actor 基本上是类型的函数 (T => Unit)。这只是函数组合(也许带有恒等函数)。

它是在 Scalaz 中实现的:

val a1 = actor(a => println(a))
val a2 = a1.comap(f)

参与者 a2 将 f 应用于其消息,然后将结果发送到 a1。

Actor is a contravariant cofunctor, so "become" is just comap.

Put another way, an Actor on messages of type T is basically a function of type (T => Unit). And this is simply function composition (with the identity function, perhaps).

It is implemented in Scalaz:

val a1 = actor(a => println(a))
val a2 = a1.comap(f)

The actor a2 applies f to its messages and then sends the result to a1.

就此别过 2024-09-17 22:03:15

在这里选择 Erlang。

在基本层面上,有两种选择。如果您只想使用 become 来更改给定进程的行为(请参阅第 2.1.3 节列表中的第 2 点),那么只需使用不同的递归函数:

loop(State) ->
     receive
          {normal, Msg} -> loop(State);
          {change, NewLoop} -> NewLoop(State)
     end.

假设 NewLoop 是一个高阶函数,每当您将消息 {change, NewLoop} 发送到最初运行函数 loop/1< 的进程时/code>,然后它将使用 NewLoop 作为其定义。

第二个选项是您希望进程充当代理(并更改行为)的选项。这与马塞洛·坎托斯的建议类似。只需让进程循环并将消息转发给新进程(窃取他的代码):

become(Pid) ->
    receive
        Msg -> Pid ! Msg
    end,
    become(Pid).

理论上,这符合论文的要求。但实际上,在现实生活中的 Erlang 系统中使用第二种选择是存在风险的。在两个进程之间的通信中,一个常见的概念是使用发送者的进程标识符“标记”消息,并且回复将使用接收者的进程标识符进行标记。可以完成以下消息的交换(这不是代码,只是手写符号):

A = <0.42.0> <-- process identifier
B = <0.54.0>,
A: {A, "hello"},
B: {B, "hi"},
A: {A, "how are you?"}.
B: {B, "Fine!"}.

因此,当 A 期望来自 B 的消息时,它将能够匹配仅通过使用诸如 {B, Message} 之类的模式来实现这些。在转发消息的情况下,该寻址方案变得无效并且简单地被破坏。

另一种方法是使用引用 (make_ref()) 作为寻址方案来匹配返回 Pids 之上的消息。这将通过使用两个不同的实体将 Pid 作为地址和标识符的使用分开。

即使寻址是安全的,还有另一个问题:进程依赖性。命名进程、崩溃进程、监视器等会发生什么?客户端进程可能有监视器、链接等所有设置,以确保在没有通知的情况下不会出现任何问题。通过修改路由进程以捕获退出信号并转发它们,应该可以使事情变得更安全:

loop(State) ->
     receive
          {normal, Msg} -> loop(State);
          {become, Pid} ->
              process_flag(trap_exit, true), % catch exit signals as messages
              link(Pid),                     % make it so if one process crashes, the other receives the signal
              become(Pid)
     end.

become(Pid) ->
    receive
        {'EXIT', Pid, killed} -> exit(self(), kill);  %% uncatchable termination
        {'EXIT', Pid, normal} -> ok;                  %% die normally too
        {'EXIT', Pid, Reason} -> exit(Reason);        %% die for the same reason as Pid
        Msg -> Pid ! Msg                              %% forward the message
    end,
    become(Pid).

这个经过测试的代码应该更安全,因为依赖于第一个进程的进程将获得与 表示的错误消息相同的错误消息中的Pid变成(Pid),使得路由相当透明。但是,我不保证这从长远来看可以在现实生活中的应用程序中发挥作用。

尽管它是可能的并且在概念上足够简单来表示和执行诸如“成为”之类的事情,但 Erlang 的标准库并没有真正考虑到第二个用例。对于现实世界的应用程序,我只能推荐第一种方法,目前存在的每个 Erlang 应用程序都广泛使用该方法。第二个不常见,可能会导致问题


**上一个示例中对函数 become/1 的调用可能应该是 ?MODULE:become (Pid) 以避免将来与热代码加载相关的潜在崩溃。 *

Going for Erlang here.

At a basic level, two options are available. If you're only wanting to use become to change the behavior of a given process (see point 2 of the list at section 2.1.3), then it's just a question of calling the next loop with a different recursive function:

loop(State) ->
     receive
          {normal, Msg} -> loop(State);
          {change, NewLoop} -> NewLoop(State)
     end.

Assuming NewLoop is a higher order function, whenever you send the message {change, NewLoop} to a process initially running the function loop/1, It will then use NewLoopas its definition.

The second option is the one where you want the process to act as a proxy (and change behavior). This is similar to what Marcelo Cantos suggested. Just have the process loop and forward messages to a new one (stealing his code):

become(Pid) ->
    receive
        Msg -> Pid ! Msg
    end,
    become(Pid).

Theoretically, this does what the paper would ask for. In practice, though, there are risks in using the second option in a real life Erlang system. In communications between two processes, it is a frequent concept to 'stamp' the message with the sender's process identifier and that the reply will be tagged with the receiver's process identifier. An exchange of the following messages could be done (this is not code, just a hand notation):

A = <0.42.0> <-- process identifier
B = <0.54.0>,
A: {A, "hello"},
B: {B, "hi"},
A: {A, "how are you?"}.
B: {B, "Fine!"}.

So when A expects a message from B, it will be able to match only for these by using a pattern such as {B, Message}. In the case of a forwarded message, this addressing scheme becomes invalid and simply broken.

An alternative would be to use references (make_ref()) as an addressing scheme to match messages on top of the returning Pids. This would separate the use of the Pid as an address and an identifier by using two different entities.

There is another problem, even if the addressing is safe: process dependencies. What happens for named processes, crashing processes, monitors, etc? The client processes might have monitors, links and whatnot all set up to make sure nothing goes wrong without being notified. By modifying the routing process to trap exit signals and forward them, it should be possible to make things safer:

loop(State) ->
     receive
          {normal, Msg} -> loop(State);
          {become, Pid} ->
              process_flag(trap_exit, true), % catch exit signals as messages
              link(Pid),                     % make it so if one process crashes, the other receives the signal
              become(Pid)
     end.

become(Pid) ->
    receive
        {'EXIT', Pid, killed} -> exit(self(), kill);  %% uncatchable termination
        {'EXIT', Pid, normal} -> ok;                  %% die normally too
        {'EXIT', Pid, Reason} -> exit(Reason);        %% die for the same reason as Pid
        Msg -> Pid ! Msg                              %% forward the message
    end,
    become(Pid).

This tested code should be safer as the processes depending on the first process will get the same error messages as the one represented by Pid in become(Pid), making the routing rather transparent. However, I wouldn't give any guarantees that this would work in the long run with real life applications.

Even though it is possible and conceptually simple enough to represent and do things like become, Erlang's standard library was just not really thought with the second use case in mind. For real world applications, I can only recommend the first method, which is used extensively by every Erlang application that exists right now. The second one is uncommon and might cause problems


**The calls to the function become/1 in the last example should likely be ?MODULE:become(Pid) to avoid potential crashes related to hot code loading in the future. *

寒冷纷飞旳雪 2024-09-17 22:03:15

请参阅 Agha 论文“Actors: A Model of Concurrent Computation in Distributed System”的第 12 页(我拥有的 PDF 副本的第 26 页)。 “成为”是他的演员语言,你如何指定#2,即演员的新行为。将消息转发给另一个参与者只是许多可能的新行为之一。

我认为,如果您想要转发行为,那么对于 Scala Actor,您基本上与 Erlang 处于同一条船上。在幕后,Scala“react”和“reactWithin”的工作方式很像become,因为react块定义的部分函数是actor的新行为,但我不确定这种相似性是否是故意的。

大多数(全部?)“演员”实现与 Hewitt 的演员模型和 Agha 的演员语言有相当大的偏差。 IIRC 在 Agha 语言中指定演员行为的语言部分甚至不是图灵完备的。只有当您考虑参与者的配置时,整个语言才变得图灵完整。我想说 Actor 模型和当前 Actor 框架之间的关系有点像 SmallTalk 中的面向对象与 C++ 中的面向对象的关系。有一些概念转移和类似的术语,但在细节上它们非常非常不同。

Look on page 12 (page 26 of the PDF copy I have) of Agha's paper "Actors: A Model of Concurrent Computation in Distributed System." "become" is his actor language is how you specify #2, the new behavior for the actor. Forwarding messages to another actor is just one of many possible new behaviors.

I think with Scala actors you're in essentially the same boat as with Erlang if you want the forwarding behavior. Under the hood, Scala "react" and "reactWithin" work much like become, because the partial function defined by the react block is the new behavior of the actor, but I'm not sure that similarity is even intentional.

Most (all?) "actor" implementations deviate fairly substantially from Hewitt's actor model and Agha's actor language. IIRC the portion of the language from specifying actors' behavior in Agha's langauge isn't even Turing complete. The language as a whole only becomes Turing complete when you consider a configuration of actors. I'd say the relationship between the actor model and current actor frameworks is kind of like the relationship of object-orientation in SmallTalk to object-orientation in C++. There's some concept transfer and similar terms, but in the details they're very, very different.

攒一口袋星星 2024-09-17 22:03:15

Akka 的 Actor 有一个“HotSwap”概念,您可以将新的 PartialFunction 发送到 Actor 来替换其现有的消息处理程序。前一个会被记住并且可以恢复。在 http://doc.akkasource.org/actors 详细信息。

Akka's Actors have a "HotSwap" concept where you can send a new PartialFunction to an Actor that replaces its existing message handler. The previous one is remembered and can be restored. Search for "hotswap" on http://doc.akkasource.org/actors for details.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文