How to have two threads communicating asynchroneously

Why bother?

An important point about Cocoa programming is that AppKit is NOT thread safe. That means that doing User Interface actions (menus, buttons, etc.) in a thread different from the main thread could lead to big problems and mysterious crashes. In my exprerience, the only safe action is to send [myView setNeedsDisplay: YES] to have a view refreshed, but you have to ensure that this does not occur during the drawRect: method (using a lock for instance). In all other cases, if you application has a worker thread doing some computations and the main thread doing the GUI, you need to be careful for cummunications in between.

Solution

The solution is to use NSConnection. Since this class is not so well documented, I explain here how I did use it for The Commandant project and FreeFem X. This works, as far as I know, but maybe there are some better solutions.

You need at least two classes. One will "live" in the main thread, and is in charge to start the connection to the worker thread, and to ensure the communication from the main thread to the worker thread (if needed). It's already implemented in the Commandant project, with name CmdApplicationGlobals. Here below I give only the thread-related parts.

The other class is named FreeFemTask in my examples, since I used it for FreeFem X. This is the class doing mainly calculations in the worker thread; sometimes, it needs to send messages to the inerface objects, hence the connection. The relevant source code is given below.

In order to have the things working, you need to copy the code below, complete the execute: method for FreefemTask (where computation takes place), and do the following:

* To start the new thread from the main thread: call [FreeFemTask startNewComputation].

* To call a method from the worker thread to the main thread (for instance, send a notification to GUI objects), call [proxy doSomething] where the method must have been defined in CmdApplicationGlobals (proxy is an outlet in FreeFemTask objects). If you have another target in mind, call
[proxy performSelector:aSelector target:aTarget argument:anArgument]
.
To send a notification use
[proxy sendNotification: aNotification object:target].
Since these action are sent to proxy, they will be executed in the main thread, not in the worker thread. Otherwise, they are just calls to similar methods in Foundation.

* To call a method from the main thread to the worker thread (for instance tell him to stop ASAP), use
[[[CmdApplicationGlobals sharedGlobals] serverObject] doSomething:parameters].
Make sure the FreeFemTask implements the method. For instance
[[[CmdApplicationGlobals sharedGlobals] serverObject] stop: exitStatus]
requires it to stop.

Implementation and explanations

INTERFACE CmdApplicationGlobals.h

@interface CmdApplicationGlobals : NSObject {
   @private
....................................
/** Used for multithread applications */
NSConnection *mtConnection;
id serverObject;
}
/** Returns the unique instance of this class */
+(CmdApplicationGlobals*) sharedGlobals;
/** Start a new Thread, enabling him to connect to the receiver via a prepared Connection.
The selector must have the form "- (void)connectWithPorts:(NSArray *)portArray",
where portArray are the ports to connect. */
-(void)startNewThreadWithSelector:(SEL)selector target:(id)target;
-(void)setServer:(id)anObject;
-(id)serverObject; /** Forward the action to the target. Used in multithreaded applications. */
-(void)performSelector:(SEL)aSelector target:(id)target argument:(id)anArgument;
/** Send a notification to the main task's default center. */
-(void)sendNotification:(NSString*) notificationName object:(id)object;
..................................................................

IMPLEMENTATION CmdApplicationGlobals.m

static CmdApplicationGlobals *theSharedGlobals;
@implementation CmdApplicationGlobals
/** Returns the unique instance of this class */
+(CmdApplicationGlobals*) sharedGlobals {
return theSharedGlobals;
} /** Start a new Thread, enabling him to connect to the receiver via a prepared Connection.
The selector must have the form "- (void)connectWithPorts:(NSArray *)portArray",
where portArray are the ports to connect. */
-(void)startNewThreadWithSelector:(SEL)selector target:(id)target {
NSArray *mtConnectionPorts;
// prepare for multithread connection
NSPort *port1 = [NSPort port];
NSPort *port2 = [NSPort port];

if (mtConnection) {
NSLog(@"ERROR startNewThreadWithSelector: app is already multithread");
return;
}
mtConnection = [[NSConnection alloc] initWithReceivePort:port1 sendPort:port2];
[mtConnection setRootObject:self];
if (!mtConnection) return; // not multithreaded : impossible

/* Ports switched here. */
mtConnectionPorts = [[NSArray arrayWithObjects:port2, port1, nil] retain];
[NSThread detachNewThreadSelector:selector toTarget:target withObject:mtConnectionPorts];
}
- (void)setServer:(id)anObject {
// [anObject setProtocolForProxy:@protocol(CalculatorMethods)];
serverObject = /*(id <CalculatorMethods>)*/[anObject retain];
if (anObject == nil) {// kill the connection
[mtConnection release];
mtConnection = nil;
}
}
-(id)serverObject {
return serverObject;
} /** Forward the action to the target. Used in multithreaded applications. */
-(void)performSelector:(SEL)aSelector target:(id)target argument:(id)anArgument {
[target performSelector:aSelector withObject:anArgument];
}
/** Send a notification to the main task's default center. */
-(void)sendNotification:(NSString*) notificationName object:(id)object {
[[NSNotificationCenter defaultCenter] postNotificationName: notificationName object:object];
}

Explanations: The more important one is startNewThreadWithSelector. This method is called by [FreeFemTask startNewComputation]. It will create a new thread an call the given selector on the given target, passing it the connection ports required for inter-thread communication. Before starting the thread (with detachNewThreadSelector), it first creates the connection. Note that this application here has only one CmdApplicationGlobals object, so there is only one worker thread (but you may want to create more, using more CmdApplicationGlobals objects). This object has a NSConnection outlet, named mtConnection. If the connection already exists, the method returns immediatly with an error message, since it means that a worker thread exists. Otherwise the connection is created using the method described in NSConnection, and the ports, in reversed order, are send to the target through selector.

The server object is kept as an outlet in the class CmdApplicationGlobals. It is really a proxy for an instance of FreeFemTask, and the method setServer must be called form the working thread (see below). Apple recommends you define a protocaol for this object (named CalculatorMethods in the documentation of NSConnection), but I didn't, as you can see from the commented part ;-). Also sending a nil object to setServer will close the connection (so a new thread can be started after). Now serverObject returns that proxy and can be used in the main thread to send messages to the worker thread, as explained before.

The two other methods are usueful to send messages or notification form the working thread to the main thread, since the working thread MUST use its proxy (which is really an instance of CmdApplicationGlobals) to do so.

Now the other class:

INTERFACE FreeFemTask.h

@interface FreeFemTask : NSObject {
@protected
CmdDocument *document;
NSString* text;
id proxy;
ffTaskState state;
@private
char receiveduseraction;
}
/** Ask to start a new computation. Return 1 if started immediatly,
0 if delayed at end of main loop. */
+(int) startNewComputation;
+(void) prepareNewComputation;
/** Current task, as seen in child thread */
+(FreeFemTask*) currentTask;
/** Current task, as seen in main thread */
+(FreeFemTask*) currentTaskMain; /** Init the unique task and gve the proxy for hooking to main thread. */
-(id)initWithProxy:(id)px;
/** Start a new computation */
-(void)execute:(id)sender;
/** Terminate current computation */
-(void)terminate:(int)exitStatus;
/** Stop current computation and kill the compiting thread. */
-(void)stop:(int)exitStatus;

IMPLEMENTATION FreeFemTask.m

FreeFemTask* CURRENT_TASK;


@implementation FreeFemTask

+(void)startNewComputationBis:(id)sender {
[FreeFemTask startNewComputation];
}
+(int) startNewComputation {
CmdDocument *doc = [CmdDocument currentDocument];
FreeFemTask *task = CURRENT_TASK;
if (!task) {// first call to startNewComputation
[FreeFemTask prepareNewComputation];
[[FreeFemTask class] performSelector:@selector(startNewComputationBis:)
withObject:nil afterDelay: 1.0];
return 0;
}
/// the task is about to start, do any required cleanup (for the main thread) here.
[[[CmdApplicationGlobals sharedGlobals] serverObject] delayedRun: nil];
return 1;// success
}
+(void) prepareNewComputation {
if (CURRENT_TASK) return; // done already
[[CmdApplicationGlobals sharedGlobals]
startNewThreadWithSelector:@selector(connectWithPorts:) target:[FreeFemTask class]];
}
+(void) connectWithPorts:(NSArray *)portArray {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSConnection *serverConnection = [NSConnection
connectionWithReceivePort:[portArray objectAtIndex:0]
sendPort:[portArray objectAtIndex:1]];
FreeFemTask *task;

task = [[FreeFemTask alloc] initWithProxy: [serverConnection rootProxy]];
CURRENT_TASK = task;
[(id)[serverConnection rootProxy] setServer:task];
[task release];// retained in setServer

[[NSRunLoop currentRunLoop] run];
/// just in case the run loop is broken; should not happen usually:
[pool release];
[NSThread exit];
}

/** Current task, as seen in child thread */
+(FreeFemTask*) currentTask {
return CURRENT_TASK;
}
/** Current task, as seen in main thread */
+(FreeFemTask*) currentTaskMain {
return [[CmdApplicationGlobals sharedGlobals] serverObject];
}

/** Init the unique task and give the proxy for hooking to main thread. */
-(id)initWithProxy:(id)px {
proxy = px;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(hasExited:) name:NSThreadWillExitNotification
object:nil ];
return self;
}
-(void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self ];
[proxy setServer: nil];
}
-(void)delayedRun: (id)sender {
[[CmdApplicationGlobals sharedGlobals] performInRunLoop:@selector(execute:)
target:self argument:sender];
}
/** Start a new computation */
-(void)execute:(id)sender {
NSAutoreleasePool *pool;
pool = [[NSAutoreleasePool alloc] init]; /// here, add code to prepare variables for computation
[proxy sendNotification: FreeFemComputingStartNotification object:self];
/// add code for all computations here [pool release];
[self terminate: 0];
}
-(int)terminateSelf:(int)exitStatus {// private /// stop your computations here
return 1;// if ok to stop
} /** Terminate current computation. Should be called only from compiting thread. */
-(void)terminate:(int)exitStatus {
if ([self terminateSelf:exitStatus])
[proxy sendNotification: FreeFemComputingEndNotification object:[self document]];
}
/** Stop current computation and kill the compiting thread. */
-(void)stop:(int)exitStatus {
[self terminate: exitStatus];
[self dealloc];//otherwise it's not called
[NSThread exit];
}


As explained before, you start the worker thread using [FreeFemTask startNewComputation]. However, this cannot be done instantly, so that one first call [FreeFemTask prepareNewComputation], and then calls itself later on in the main run loop to have everything prepared. The preparation consists in a call to startNewThreadWithSelector described in the other class, with the FreeFemTask class as target and connectWithPorts as action. That one will be called inside the fresly created thread. It basically creates another connection corresponding to the connection in CmdApplicationGlobals, and a new FreeFemTask object. Since there is only one working thread here, it is also stored in a global variable. Also connectWithPorts creates an AutoreleasePool and a run loop for the worker thread.

On the second call of [FreeFemTask startNewComputation], the task object should exists and be inited. It is then send a delayedRun action, through the proxy [[CmdApplicationGlobals sharedGlobals] serverObject] (remember we are in the main thread for startNewComputation). delayedRun could be used from any object in the main thread (even a menu, button, etc.); it is just a call to execute: through the proxy again.

Anyway, execute is reached in the worker thread and can proceed to the real computations.

Note also the terminate: method (which must be called form the worker thread, but send notification to object in main thread), and stop: (which also stops the thread).

If you are a little bit lost during execution, you can add logs in your code:
NSLog(@"%@ %@, thread=%@", self, NSStringFromSelector(_cmd), [NSThread currentThread]);
This helps understanding who does what.


T. Lachand-Robert, 01-Jul-2001.