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

Download yamessagequeue.zip.

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 NSPort to communicate.
  • InterThreadMessaing is based on notifications.
  • ThreadWorker spawns a new thread for each message. It serves a different purpose.