Asynchronous Thread Messaging
Multi-threaded programming is hard. Mac OS X provides
many tools to help writing a multi-threaded application, but
you have to be careful. If you jump right in and use
NSThread and NSLock to add threads and
make your objects thread safe, chances are you will introduce bugs.
I know, I did. Thread related bugs are always hard to find. They
tend to be hard to reproduce, and often they only appear on
specific hardware.
Recently, I have taken a different approach: I fire up a few threads when my appliction starts. Each thread is responsible for certain tasks that need to be done in the background. I share as few objects as possible between threads, and I use asynchronous messages to communicate. This works well, because I usually don't have to write thread-safe classes, and asynchronous messages don't cause dead-locks.
You can send asynchronous messages with
Distributed Objects using oneway void functions,
but I wrote my own light-weight implementation.
The problem with Distributed Objects messaging is that arguments are archived and appear as an unarchived copy or as a proxy on the other side. This is necessary when communicating between different processes, but for threads it is not. Sometimes you want to pass real objects, and sometimes you don't want the overhead of archiving large data structures.
Of course there is a danger to passing real objects between
threads. If you send an NSMutableString to another
thread, and both threads try to modify the string, you are in
trouble. Stick to immutable containers and other thread-safe
objects, and you should be fine.
YAMessageQueue
Each thread that wants to receive asynchronous messages has an
associated YAMessageQueue. The message queue receives
messages from other threads and delivers them in order to the
receiving thread, using the run loop. You can get the message queue
for the current thread using the +[YAMessageQueue
defaultQueue] class method, but sometimes it can be an
advantage to create the message queue from another thread. Take a
look at the following class:
@implementation BackgroundThread
- (YAMessageQueue*)messageQueue
{
return messageQueue;
}
- (id)init
{
if (![super init])
return nil;
messageQueue = [[YAMessageQueue alloc] init];
[NSThread detachNewThreadSelector:@selector(threadMain:) toTarget:self withObject:nil];
return self;
}
- (void)threadMain:(id)unused
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[messageQueue deliverInCurrentThread];
[[NSRunLoop currentRunLoop] run];
[pool release];
}
The line [messageQueue deliverInCurrentThread]
tells the message queue to deliver messages in the newly detached
thread, but the message queue was created in the main thread, and
it can be used immediately:
BackgroundThread *bt = [[BackgroundThread alloc] init];
YAMessageQueue *q = [bt messageQueue];
[[q proxyForTarget:someObj] setTitle:@"Hello World"];
You can send messages to the thread even when it might not be running yet (asynchronously, remember?). That makes startup a lot faster, there is no waiting around for the new thread to initialize.
Sending messages
As you saw above, sending messages to another thread is very
simple. To send a message to the object obj that is to
be executed on the thread associated with the message queue
q, simply say:
[[q proxyForTarget:obj] myMessage:@"hello" arg2:5];
The proxies are light-weight objects. They simply hold a reference to the message queue and the target, so go ahead and create a proxy every time you need to send a message. Of course you can retain a proxy, but note that the proxy retains the target, so it is easy to create retain loops:
asyncSelf = [[q proxyForTarget:self] retain]; // BAD: retain loop
I really should add a method to the proxy to have it release the
target reference. One case where that would be useful is when you
register the proxy to receive notifications from
NSNotificationCenter. The proxy has to be retained
because the notification center doesn't do it, and the retain loop
is hard to avoid.
Messages are asynchronous, just like oneway
void methods. That means that sending the message returns
immediately, and it is delivered at a later time. It also means
that you won't get useful return values from non-void
methods.
Setting a protocol
The message queue does not give 100% thread isolation. The
messages you send through the proxy are delivered on the proper
thread, but every time -[methodSignatureForSelector:]
is sent to the target in the calling thread. Normally that
is not a problem, the default NSObject implementation
is perfectly thread safe, but if the target happens to be, say, a
proxy for a Ruby object, you could be in trouble. (The Ruby
interpreter is very much single threaded). In that special case,
you can send -[setProtocolForProxy:] to the proxy.
When a protocol is set, the proxy can only handle methods defined
by the protocol, and it will get method signatures from the
protocol. The target object is never sent any messages in the wrong
thread.
Getting the code
Just include the source files YAMessageQueue.m and
Protocol-YASignature.m in your XCode project. There is
no need for frameworks and other such nonsense.
Alternatives
There are some other message passing schemes floating about:
- ThreadMessage is
very similar to this implementation. It passes arguments the same
way (no copying). It has no proxies, and it uses
NSPortto communicate. - InterThreadMessaing is based on notifications.
- ThreadWorker spawns a new thread for each message. It serves a different purpose.