A core tenet of object-oriented programming is that objects encapsulate some state, plus some behavior, and that objects communicate with each other through messages: if object A wants to communicate with object B, then A sends a message to B.
This simple model of object communication, however, says nothing about where A and B might be located. If A and B both execute in the same address space, then A and B can exchange message via local method calls. In a concurrent, distributed environment, however, A and B might be located in different address spaces, even on different hosts. In that situation, A and B might communicate via distributed messages.
Some programming languages emphasize the difference between message-based vs function-invocation-based communication, writes Tony Arcieri, creator of the Reia language, in a recent blog post, Why I Don't Like Scala.
Scala's Actors, for example, provide three different models for objects to exchange messages:
Because the JVM isn't stackless and uses native threads as its concurrency mechanism, Scala couldn't implement Erlang-style lightweight concurrency, and instead compromises by implementing two types of actors. One type of actor is based on native threads. However, native threads are substantially heavier than a lightweight Erlang process, limiting the number of thread-based actors you can create in Scala as compared to Erlang. To address this limitation, Scala implements its own form of lightweight concurrency in the form of event-based actors. Event-based actors do not have all the functionality of thread-based actors but do not incur the penalty of needing to be backed by a native thread.
Should you use a thread-based actor or an event-based actor in your Scala program? This is a case of implementation details (namely the JVM's lack of lightweight concurrency) creeping out into the language design...
Scala leaves you with three similar, overlapping constructs to choose from when modeling state, identity, and concurrency in programs: objects, event-based actors, and thread-based actors. Which should you choose?
By contrast, Arcieri's language, Reia, which runs on the Erlang VM, turns all objects into Actors, in effect. At the same time, Reia introduces many notions of OO programming that developers may at first find strange.
What do you think of Arcieri's notion that concurrency should be modeled at the object level in a language?
The best approach to actor concurrency is with objects. As I have said previously here http://www.artima.com/forums/flat.jsp?forum=270&thread=251341: The Actor model, when combined with Object Orientation, provides the most elegant solution for concurrency.
Non-concurrent programs can be made concurrent by simply replacing all objects with Actor equivalents. Any concurrency in the program is then automatically revealed .
> > Non-concurrent programs can be made concurrent by simply > replacing all objects with Actor equivalents. Any > concurrency in the program is then automatically revealed > . >
Only if you've already done all the hard work of making sure the program computes things well before actually using them, eg rewriting things like
for(int i = 0; i < MAX; ++i) { printf("result %i = %i\n", i, Int(2000000000).sum()); }
to instead look like
int results[MAX]; for(int i=0; i < MAX; ++i) { results[i] = Int(2000000000).sum(); } for(int i = 0; i < MAX; ++i) { printf("result %i = %i\n", i, results[i]); }
, which is then trivially changed to
unique_future<int> results[MAX]; for(int i=0; i < MAX; ++i) { results[i] = Int(2000000000).sum(); } for(int i = 0; i < MAX; ++i) { printf("result %i = %i\n", i, results[i].get()); }
to "automatically reveal" the hidden concurrency you just painstakingly added with the rewrite.
> <p>What do you think of Arcieri's notion that concurrency > should be modeled at the object level in a language?</p>
I don't think that any general-purpose language should model concurrency in some specific way. The core language should provide the primitives for concurrency and the libraries should use those primitives to implement various concurrency models that support different needs. For example, such approach is taken by C++0x and I think that's the right way to do it.
> One of the most common patterns in actor-based programs > is the Remote Procedure Call or RPC. As the actor > protocol is asynchronous, RPCs provide synchronous calls > in the form of two asynchronous messages, a request and > a response. > > 95% of the time standard synchronous RPCs will work
This is interesting, in that actors using RPC are no more concurrent than objects using method calls. Actually, they're less concurrent, because having one and only one thread per actor means that all your methods are inherently synchronized whether that's needed or not.
> Reia is a language which asks you to sit back for a > second and ponder what can be modeled as possibly nested > structures of lists, tuples, and maps instead of objects.
But I thought lists, maps, etc, were objects?
> Reia does not allow cyclical call graphs, meaning that > an object receiving a call cannot call another object > earlier in the call graph.
Yes, this is what happens when all your methods are synchronized and your locks can't be made thread-aware (to specifically permit reentrancy).
> Programmers shouldn't even need to use actors directly > unless they're implementing certain actor-specific > behaviors, like FSMs (the 5% case Joe Armstrong was > talking about).
So, how to make the majority case work better (no deadlocks on call cycles), preferably without impacting the minority case where you really do want asynchronous messages...
How about: 1) calls are done as usual for OO languages 2) all methods are synchronized 3) asynchronous message sending is the special case, and is the same as spawning a thread or sending a task to a thread pool.
I think this should be identical to the standard "each actor (object) has a select loop" version of actors, except for allowing cyclic call graphs (like mutual recursion).
Item 2 should preserve the behavior of "one thread per object (actor)".
Item 1 means that there are actual threads that follow the flow of control, so your mutexes underlying the synchronized methods can allow reentrancy.
Item 3 is what grants concurrency. It doesn't require changes to other code (like replacing . with !?).
I suppose this approach would require programmer awareness and cooperation for true distributed use with multiple address spaces, since (1) doesn't work across different address spaces. Or maybe not, since with cooperation from the runtime (including thread IDs that include a machine identifier) it could be transparently implemented as pairs of messages when needed... but this could do interesting things to performance when a particular call is suddenly serializing your 10 GB hashtable instead of passing it by const reference.
> How about: > 1) calls are done as usual for OO languages > 2) all methods are synchronized > 3) asynchronous message sending is the special case, and > is the same as spawning a thread or sending a task to a > thread pool. > > I think this should be identical to the standard "each > actor (object) has a select loop" version of > actors, except for allowing cyclic call graphs (like > mutual recursion). > > Item 2 should preserve the behavior of "one thread per > object (actor)". > > Item 1 means that there are actual threads that follow the > flow of control, so your mutexes underlying the > synchronized methods can allow reentrancy. > > Item 3 is what grants concurrency. It doesn't require > changes to other code (like replacing . with !?). > This approach is very similar to the following...
Instead of having an operator that indicates asynchronous method calls, methods are declared asynchronous in their class definition. That way, the object itself is responsible for its concurrent behaviour instead of the caller. The only difference to your definitions is that threads are not bound to objects. Threads and objects are inherently different things so I don't think its wise to pretend that they are similar.
My favorite programming language which I've never written a program in is called Ellie. It is a fine-grained parallel object-based language, originally designed to run on the Transputer. You can find papers on it at ftp://ftp.diku.dk/diku/distlab/ellie/ . In Ellie everything is an object, including the control structures, and every object invocation can be done in parallel using futures. I like it largely for the same reason that I like Scheme: it packs a lot of power into a small number of concepts. I always meant to put together a Java compiler for it and call it Jellie, but like so many personal projects I never quite got around to it.
> > > > Non-concurrent programs can be made concurrent by > simply > > replacing all objects with Actor equivalents. Any > > concurrency in the program is then automatically > revealed > > . > > > > Only if you've already done all the hard work of making > sure the program computes things well before actually > using them, eg rewriting things like >
> for(int i = 0; i < MAX; ++i) { > printf("result %i = %i\n", i, Int(2000000000).sum()); > } >
> to instead look like >
> int results[MAX]; > for(int i=0; i < MAX; ++i) { > results[i] = Int(2000000000).sum(); > } > for(int i = 0; i < MAX; ++i) { > printf("result %i = %i\n", i, results[i]); > } >
> , which is then trivially changed to >
> unique_future<int> results[MAX]; > for(int i=0; i < MAX; ++i) { > results[i] = Int(2000000000).sum(); > } > for(int i = 0; i < MAX; ++i) { > printf("result %i = %i\n", i, results[i].get()); > } >
> to "automatically reveal" the hidden concurrency you just > painstakingly added with the rewrite.
No, you did not understand my comment. In an truly object-oriented language where each method is a message, automatically changing the method invocation algorithm from a direct call which yields a result to an message posting + future result, you get the hidden concurrency to be fully automated.
When I say "automatically changing", I mean the compiler does the dirty job, you don't have to program anything.
> > > I also had a C++ demo here: > > http://www.mediafire.com/download.php?w3qn4mrghiz > > > > If it's good in C++, then in other more modern > languages, > > it will be super. > > Is that Actors or Futures? Or Actors pretending to be > Futures?
Both actors and futures are required. An actor contains the state, a future contains the future result.
> So, how to make the majority case work better (no > deadlocks on call cycles), preferably without impacting > the minority case where you really do want asynchronous > messages... > > How about: > 1) calls are done as usual for OO languages > 2) all methods are synchronized > 3) asynchronous message sending is the special case, and > is the same as spawning a thread or sending a task to a > thread pool.
You don't have to do that. All you have to do is enter a new message loop when waiting for a result. In this way, deadlocks can't happen, and the only problem you have is stack overflow, which can be solved if you pack the rest of the computation as a continuation message.
> No, you did not understand my comment. In an truly > object-oriented language where each method is a message, > automatically changing the method invocation algorithm > from a direct call which yields a result to an message > posting + future result, you get the hidden concurrency > to be fully automated.
Which is quite irrelevant, as the hard part is making there be any (safe) concurrency -- hidden or not -- available in the first place.
This is serial code, futures won't help it:
int sum(int low, int high) { int ret = 0; for (int i = low; i < high; ++i) ret += i; return ret; }
This is parallel(-izable) code, futures can help:
int sum(int low, int high) { int size = high - low; if (size < 5 /* arbitrary cutoff, must be >= 2 */) { int ret = 0; for (int i = low; i < high; ++i) ret += i; return ret; } else { int a = sum(low, low + (size/2)); int b = sum(low + (size/2), high); return a + b; } }
Do you see why I say it doesn't matter whether I have to write something like future<int> a = pool.run(& sum, low, low + (size/2)); rather than int a = sum(low, low + (size/2));? That's a trivial change, the hard part is rearranging the code to make it possible for either me or the compiler to make it.
oo vs. procedural, messages vs. calls, are completely irrelevant. All that's relevant is storing your results without using them while you do unrelated work.
> When I say "automatically changing", I mean the compiler > does the dirty job, you don't have to program anything.
Does this compiler know whether any of the method invocations it's parallelizing (and therefore possibly reordering) have side effects, like reading from stdin or writing to stdout? Does it know that in
it's a really bad idea to parallelize the call to foo() since that could let the lookup happen after the map is cleared? You could add a result.get() immediately before stuff.clear() to force a wait until the lookup is done, but that would bring things back to the programmer rather than the compiler doing the dirty job, and in a way that gives bugs rather than slowness when you miss something.
What you're arguing for here, can only be safe if there are no side effects (which would include mutable state, even in the form of function parameters like in part 11 of http://tamale.net/erlang/tutorial.shtml). That's pure functional programming, not oo/actors.
It can only be useful if the programmer has written code with parallelism in mind, using algorithms that divide work into independent chunks rather than spinning through it serially. The effort that your automatic futurization saves is trivial in comparison, and comes at the expense of correctness.
********************
> All you have to do is enter a new message loop when > waiting for a result. In this way, deadlocks can't happen, > and the only problem you have is stack overflow, which can > be solved if you pack the rest of the computation as a > continuation message.
It has to be packed as a continuation message anyway, in case you're ready to finish it before you're ready to finish the next one.
When your callstack looks like this:
dispatch() fnord() + 123 # waiting on message to continue wait_and_dispatch() bar() + 456 # waiting on message to continue wait_and_dispatch()
what happens when you get the message fnord() is waiting for, and won't get the message bar() is waiting for for another couple seconds (or worse, until you do something with the message for fnord())?
If your objects/actors contain state (and your messge loops are implicit), going back to the message loop allows threads to step on eachother.
If you're using a language where objects/actors don't have state, then you'll be storing any "state" in the parameters to your (explicit) dispatch function. Extra implicit calls or returns to the dispatcher would break this horribly, so you're back to not being able to process any messages except the one you're specifically waiting for to continue with the current function.
> Do you see why I say it doesn't matter whether I have to > write something like future<int> a = pool.run(& sum, > low, low + (size/2)); rather than int a = > sum(low, low + (size/2));? That's a trivial change, > the hard part is rearranging the code to make it possible > for either me or the compiler to make it.
I never said that actors will magically make algorithms parallelizable. I said that actors will reveal any hidden parallelism that is there in the code.
> > When I say "automatically changing", I mean the > compiler > > does the dirty job, you don't have to program anything. > > Does this compiler know whether any of the method > invocations it's parallelizing (and therefore possibly > reordering) have side effects, like reading from stdin or > writing to stdout? Does it know that in >
> it's a really bad idea to parallelize the call to > foo() since that could let the lookup happen > after the map is cleared? You could add a > result.get() immediately before > stuff.clear() to force a wait until the > lookup is done, but that would bring things back to the > programmer rather than the compiler doing the dirty job, > and in a way that gives bugs rather than slowness > when you miss something.
No, you have it wrong. The code above will work perfectly, because stuff[k] will be put in stuff's message queue, and then clear() will be put in stuff's message queue, and they will be executed one after the other.
> > > What you're arguing for here, can only be safe if > there are no side effects (which would include mutable > state, even in the form of function parameters like in > part 11 of http://tamale.net/erlang/tutorial.shtml). > That's pure functional programming, not oo/actors.
No. The OO actor model is safe by definition, because attributes of an actor object are not accessible from other objects.
> > It can only be useful if the programmer has written > code with parallelism in mind, using algorithms that > divide work into independent chunks rather than spinning > through it serially. The effort that your automatic > futurization saves is trivial in comparison, and comes at > the expense of correctness.
For most tasks, automatic concurrency derived from OO actors is more than enough. Some algorithms will need modifications, especially those that process arrays. But, overall, the OO actor model is a huge win for the programmer.
> > ******************** > > > All you have to do is enter a new message loop when > > waiting for a result. In this way, deadlocks can't > happen, > > and the only problem you have is stack overflow, which > can > > be solved if you pack the rest of the computation as a > > continuation message. > > It has to be packed as a continuation message anyway, in > case you're ready to finish it before you're ready to > finish the next one. > > When your callstack looks like this: >
> dispatch() > fnord() + 123 # waiting on message to continue > wait_and_dispatch() > bar() + 456 # waiting on message to continue > wait_and_dispatch() >
> what happens when you get the message fnord() > is waiting for, and won't get the message > bar() is waiting for for another couple > seconds (or worse, until you do something with the message > for fnord())?
The only case an actor waits is when waiting for a result. So, if fnord() is waiting, it means it is waiting for a result. In the mean time, it can process other messages. Bar() will not have been executed yet, as the actor waits for fnord() to finish.
> If your objects/actors contain state (and your messge > loops are implicit), going back to the message loop allows > threads to step on eachother.
That's no different than successively calling functions on an object that can modify its state. For example:
class A { int data;
void foo(B b) { data = 5; B.bar(this); }
void bar() { data = 3; } }
class B { void bar(A a) { a.bar(); } }
In the above example, an object calls a method of another object, which in turn calls a method of the first object that modifies the first object's state. Thus, in foo(), the member data is implicitly modified.
Same thing happens when an actor processes other messages when waiting for a result.
> No, you have it wrong. The code above will work perfectly, > because stuff[k] will be put in stuff's message queue, and > then clear() will be put in stuff's message queue, and > they will be executed one after the other.
I guess I elided too much, then. Perhaps this will be better:
class /*actor*/ Map { map<Key, Thing> stuff; public: Thing lookup(Key k) { return stuff[k]; } void clear() { stuff.clear(); } }; class /*actor*/ Class1 { Thing foo(int x, Map & m) { Key k(x, ...); return m.lookup(k); } }; class /*actor*/ Class2 { Map & m; Class1 & c; void bar() { Thing result = c.foo(123, m); m.clear(); } };
The point being whether the call to foo() (here Class1::foo()) is safe to parallelize, as doing so would mean that stuff[k] and stuff.clear() (here Map::lookup() and Map::clear()) would be called (or put in the message queue) from different threads, with no guaranteed ordering.
thread for | thread for | thread for Class2 | Class1 | Map enter "bar" | | send "foo" | enter "foo" | send "clear" | send "lookup" | did "lookup" or "clear" arrive first?
> No. The OO actor model is safe by definition, because > attributes of an actor object are not accessible from > other objects.
What does that have to do with anything? It does nothing to prevent your side effects and state changes from being possibly reordered.
> > ********************
> The only case an actor waits is when waiting for a result. > So, if fnord() is waiting, it means it is waiting for a > result. In the mean time, it can process other messages. > Bar() will not have been executed yet, as the actor waits > for fnord() to finish.
So then what does it do when it processes these other messages, since you say it doesn't execute anything as a result of them?
> > If your objects/actors contain state (and your messge > > loops are implicit), going back to the message loop allows > > threads to step on eachother.
> That's no different than successively calling functions on > an object that can modify its state.
What it means is that in
class A { int data; void set(int x) { data = x; } void do_stuff(B b) { int tmp = b.foo(data * 5); if (tmp < data) { ... } data = ...; } }; class B { int foo(tmp) { /* something that has nothing at all to do with A */ } };
your call to set() can happen in the middle of my call to do_stuff(). Implicitly calling the message loop in the middle of a function means that you're back to needing explicit locking to make your functions thread-safe.
> > No, you have it wrong. The code above will work > perfectly, > > because stuff[k] will be put in stuff's message queue, > and > > then clear() will be put in stuff's message queue, and > > they will be executed one after the other. > > I guess I elided too much, then. Perhaps this will be > better: >
> The point being whether the call to foo() > (here Class1::foo()) is safe to parallelize, > as doing so would mean that stuff[k] and > stuff.clear() (here > Map::lookup() and Map::clear()) > would be called (or put in the message queue) from > different threads, with no guaranteed ordering. >
> thread for | thread for | thread for > Class2 | Class1 | Map > enter "bar" | | > send "foo" | enter "foo" | > send "clear" | send "lookup" | did "lookup" or "clear" > arrive first? >
I agree with you on this, but I never said that algorithms with actors are equal to algorithms without actors. I said that any parallelism in a sequentially written code is automatically revealed.
When using actors, the order of operations is not guaranteed, except when using results. So, a very cheap way to force a sequence on an actor is to operate on the result, which can be the called object.
Another solution is to use a third actor to serialize the order of operations. Since object state is protected, implementing a simple state machine with actors is trivial.
> > > > No. The OO actor model is safe by definition, because > > attributes of an actor object are not accessible from > > other objects. > > What does that have to do with anything? It does nothing > to prevent your side effects and state changes from being > possibly reordered.
It allows you to model a state machine actor that coordinates execution very easily.
> > > > ******************** > > > The only case an actor waits is when waiting for a > result. > > So, if fnord() is waiting, it means it is waiting for a > > result. In the mean time, it can process other > messages. > > Bar() will not have been executed yet, as the actor > waits > > for fnord() to finish. > > So then what does it do when it processes these > other messages, since you say it doesn't execute anything > as a result of them? > > > > If your objects/actors contain state (and your messge > > > loops are implicit), going back to the message loop > allows > > > threads to step on eachother. > > > That's no different than successively calling functions > on > > an object that can modify its state. > > What it means is that in >
> class A { > int data; > void set(int x) { > data = x; > } > void do_stuff(B b) { > int tmp = b.foo(data * 5); > if (tmp < data) { > ... > } > data = ...; > } > }; > class B { > int foo(tmp) { > /* something that has nothing at all to do with A */ > } > }; >
> your call to set() can happen in the > middle of my call to do_stuff(). > Implicitly calling the message loop in the middle of a > function means that you're back to needing explicit > locking to make your functions thread-safe.
No, you don't need any locking. All you need is to copy the state locally before using it.
> I agree with you on this, but I never said that algorithms > with actors are equal to algorithms without actors. I said > that any parallelism in a sequentially written code is > automatically revealed.
My understanding is that research has shown that there is very little inherent parallelism in even functional code. If that's true, automatically revealing what is there isn't going to amount to much.
It seems to me that most code doesn't provide enough clues about what can be done in tandem and what must be done sequentially.
> I agree with you on this, but I never said that algorithms > with actors are equal to algorithms without actors. I said > that any parallelism in a sequentially written code is > automatically revealed. > > When using actors, the order of operations is not > guaranteed, except when using results. So, a very cheap > way to force a sequence on an actor is to operate on the > result, which can be the called object. > > Another solution is to use a third actor to serialize the > order of operations.
But now you're rewriting your code so that the parallelization doesn't break it. Which means that the only thing that was "automatically revealed" was a bunch of bugs that you have to fix to really reveal any parallelism. (Which as James says, and I tried to illustrate earlier with the sum() example, there won't be very much of unless you explicitly wrote it into your code in the first place.)
> It allows you to model a state machine actor that > coordinates execution very easily.
Which has what, exactly, to do with being safe (from side-effect reordering) by definition? It can be made safe by adding synchronization code, but so can anything else.
> > your call to set() can happen in the > > middle of my call to do_stuff(). > > Implicitly calling the message loop in the middle of a > > function means that you're back to needing explicit > > locking to make your functions thread-safe. > > No, you don't need any locking. All you need is to copy > the state locally before using it.
Are you seriously saying that you can take a non-atomic read-modify-write and make it atomic without locking?
Flat View: This topic has 44 replies
on 3 pages
[
123
|
»
]