How is "become" implemented in languages that support the actor model?
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 im开发者_运维问答plemented 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)?
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.
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.
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 NewLoop
as 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. *
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.
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.
精彩评论