The Actor model of programming was presented in 1973 by Carl Hewitt, Peter Bishop, and Richard Steiger. You can find their original paper online and their ideas can be found in many contemporary programming languages, including Erlang and Io.
An actor is an abstract encapsulation of computation that makes it easier to represent multiple concurrent streams of control. One way to see actors is as objects that run in their own dedicated threads; messages are sent asynchronously from actor to actor, and each actor concurrently receives and responds to its messages, often by sending new messages to other actors.
I’ve written a short Nu program that sketches a pattern for actor-oriented programming in Objective-C. Except for its use of Nu blocks (a.k.a. anonymous functions), my entire example could be easily rewritten in Objective-C, and with a certain loss of flexibility, message selectors could be used instead of Nu blocks to build a purely Objective-C solution. But once you’ve seen it in Nu, you may not want to go back.
Design
The example program plays the “telephone game”. MessagePasser actors are arranged in a ring that begins and ends at a Director who starts the game by sending a message to the first actor. That actor then receives the message on its thread, makes a simple modification to it, and passes it on to the next actor. This proceeds until the message gets back to the Director.
Implementation
First we have a general abstraction for an actor, the Actor class:
(class Actor is NSObject
(ivar (id) thread)
(- (id) init is
(super init)
(set @thread ((NSThread alloc) initWithTarget:self
selector:"run:" object:nil))
self)
(- (void) run:(id) argument is
(until (@thread isCancelled)
((NSRunLoop currentRunLoop) runUntilDate:
(NSDate dateWithTimeIntervalSinceNow:0.1))))
(- (void) start is (@thread start))
(- (void) stop is (@thread cancel))
(- (void) perform:(id) block is
(self performSelector:"_perform:" onThread:@thread
withObject:block waitUntilDone:NO))
(- (void) _perform:(id) block is (block self)))
Our general-purpose actor has a single instance variable, the NSThread in which it runs. We’ll create the thread when the actor is created, but we won’t start it until the actor receives the start message. When it is started, the actor’s thread begins executing the run: method, which uses Cocoa’s NSRunLoop class to enter an event-driven loop where it waits for messages. Messages are sent to the actor using the perform: method, which takes a Nu block as an argument. It relays the block it receives to the _perform: method, which it ensures is evaluated on the actor’s thread. The perform: method itself is run on the thread of whatever is sending the message. We use Cocoa’s performSelector:onThread:withObject:waitUntilDone: method to safely pass that message between threads; it requires a run loop in the receiving thread to receive the message.
Next we’ll create the Actor that will play our game, the MessagePasser:
(class MessagePasser is Actor
(ivar (id) receiver (int) index)
(ivar-accessors)
(- (id) initWithIndex:(int) index is
(super init)
(set @index index)
self)
(- (void) relay:(id) message is
(@receiver perform:
(do (actor) (actor relay:(+ message @index))))))
This is a very simple subclass that adds a receiver and an index for each actor. The index is just used as an identifier and the receiver links the actor to the next object in the chain, the one who should receive its relayed message. The relay: message does the interesting work. Running on the actor’s thread, it uses the Nu do operator to construct an anonymous function that it sends to the receiver for evaluation. That function simply adds the actor’s index to the message and sends the relay message to the next actor in the chain. In this way, we send our message along the entire chain of actors until it reaches the final object, which we define next, the Director:
(class Director is NSObject
(ivar (id) actors (int) done)
(ivar-accessors)
(- (id) initWithCount:(int) count is
(super init)
(set @actors (array))
(NSLog "creating actors")
(count times:
(do (i)
(@actors addObject:
((MessagePasser alloc)
initWithIndex:(+ i 1)))))
(NSLog "organizing actors")
((- count 1) times:
(do (i) ((@actors i) setReceiver:(@actors (+ i 1)))))
((@actors lastObject) setReceiver:self)
(NSLog "starting actor threads")
(@actors each:(do (actor) (actor start)))
(NSLog "ready")
self)
(- (void) relay:(id) message is
(set @done YES)
(NSLog (+ "received: " message)))
(- (void) perform:(id) block is
(self performSelectorOnMainThread:"_perform:"
withObject:block waitUntilDone:NO))
(- (void) _perform:(id) block is
(block self))
(- (void) start:(id) message is
(set @done NO)
(NSLog (+ "sending: " message))
((@actors objectAtIndex:0) perform:
(do (actor) (actor relay:message))))
(- (void) run:(id) message is
(self start:message)
(until @done
((NSRunLoop mainRunLoop)
runUntilDate:(NSDate dateWithTimeIntervalSinceNow:0.1)))))
We’ll put our director in charge of all our actors, and it will keep them in an NSArray associated with the @actors instance variable. The initWithCount: method creates the specified number of actors, links them together in a chain, and sets the receiver of the last one to be the director itself. That means that our director needs relay: and perform: methods; they are defined next. Our relay: method just marks the message sending as finished and perform: takes the received message and runs it on the main application thread. start: is used internally to send the message down the line, and run: is the externally-available method that is sent to start a message and wait for its completion. In a typical Cocoa application, we wouldn’t want to include a run loop here, but we’re just going to run this in the Nu shell, and without a run loop on the main thread, the message sent by performSelectorOnMainThread:withObject:waitUntilDone: won’t be executed.
Testing
Now it’s time for the fun. Put all the above code in a single file; call it “actor.nu”. Then you can run it in the Nu shell by starting nush with the following command:
% nush actor.nu -i
That starts nush, loads the actor.nu source file, and brings up an interactive prompt. Now lets create a director and have it make some actors.
Nu Shell. % (set d ((Director alloc) initWithCount:500)) 2008-03-02 20:56:25.497 nush[51616:807] creating actors 2008-03-02 20:56:25.569 nush[51616:807] organizing actors 2008-03-02 20:56:25.808 nush[51616:807] starting actor threads 2008-03-02 20:56:28.881 nush[51616:807] ready <Director:36d950>
We just created a chain of 500 actors to play in our message passing game. Since each actor has its own thread, we’ve created and started 500 process threads. This seems to be pushing the limits to me; as far as I’m concerned, the best use of the actor model is to manage the concurrency of multiprocessor systems, and that requires only roughly the same number of actors as we have processors. But let’s do this anyway.
Next we want to send a message down the chain.
% (d run:0) 2008-03-02 21:00:07.186 nush[51616:807] sending: 0 2008-03-02 21:00:15.458 nush[51616:807] received: 125250 ()
That took a little over 8 seconds on my 2.4GHz/2GB MacBook Pro. Since we numbered our actors from 1 to 500 and each actor added its number to our message, let’s confirm that we got the right sum:
% (* 500 0.5 (+ 1 500)) 125250
Now let’s give in to the temptation to send our message again.
% (d run:0) 2008-03-02 21:03:13.970 nush[51616:807] sending: 0 2008-03-02 21:03:14.082 nush[51616:807] received: 125250 ()
Did you notice how much faster that was? Do it a few more times; now the results are almost instantaneous. To be sure that we’re still going through the chain, send a different starting value:
% (d run:-125250) 2008-03-02 21:04:25.881 nush[51616:807] sending: -125250 2008-03-02 21:04:25.997 nush[51616:807] received: 0 ()
Also, since Nu additions have a bit of polymorphism, for fun let’s send an empty string as our message:
% (d run:"") 2008-03-02 21:05:25.628 nush[51616:807] sending: 2008-03-02 21:05:25.777 nush[51616:807] received: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500 ()
You can also run “top” in another terminal window to see that our example program really is running 500 actor threads (plus one for the main thread).
Why was that first chain of message sends so much slower? I don’t know, but the results are consistent each time I run this example. I suspect that Cocoa is somehow lazily initializing each of those 500 run loops and doing some extra setup work the first time they receive an event. Anyone from Apple want to comment on that?
Conclusions
This was a simple example, but one with a surprisingly impressive result. We’ve demonstrated 500 concurrent actors, each running in its own process thread, all built and run in an interpreted scripting language. Feel free to reproduce this in Objective-C or your favorite scripting language and post your results; I’d like to see how it translates to other environments. Also, what next? Are there other sample problems that would provide other good demonstrations? And Objective-C experts, note that Nu blocks are lambda expressions that allow us to pass arbitrary control expressions from actor to actor. How would you do this from Objective-C?


Comment on this post ↓
Leave a Comment (sign in with Twitter)