Mulle kybernetiK - Tech Info: v0.0
Obj-C Optimization: Concurrent Programming, a quick refresher
This sixth installment of the ongoing series "This is just a quick refresher on concurrent programming: Why ++myVariable is not thread safe..
(c) 2002 Mulle kybernetiK - text by Nat!
Lameness Disclaimer: All this is written to the best of my knowledge. Corrections, additions etc. are certainly welcome.

Concurrent Programming, a quick refresher

As you know, you should protect shared mutable data with locks as explained in Cocoa/TasksAndConcepts/ProgrammingTopics/Multithreading. Even just incrementing a global variable

static int  numberOfFiles;

void  incrementNumberOfFilesRead()

is not thread safe at all. This one C-instruction is actually executed with three or more PPC instructions that look similar or identical to this:

	lwz	r11,0(r9)
	addi	r0,r11,1
	stw	r0,0(r9)

While each PPC instruction is executed there is a possibility that an interrupt occurs. The processing of this thread will then be suspended and the processor's attention will be focused on another thread. This other thread might very well now execute the same code trying to access the same global variable!

To show how this can be disastrous, lets examine two cases where two threads execute the same code. In the first example the context switch to the second thread is harmless, because the increment statements have completed:

thread #0 thread #1 variable contents
lwz r11,0(r9) 0 (read)
addi r0,r11,1
stw r0,0(r9) 1 (written)
lwz r11,0(r9) 1 (read)
addi r0,r11,1
stw r0,0(r9) 2 (written)

but it could also happen, that the context switch occurs right in the middle of the increment instruction sequence:

thread #0 thread #1 variable contents
lwz r11,0(r9) 0 (read)
lwz r11,0(r9) 0 (read)
addi r0,r11,1
stw r0,0(r9) 1 (written)
addi r0,r11,1
stw r0,0(r9) 1 (written)

As you can see the outcome is not the same!

Using Foundation to share common resources

The general solution to share a resource between threads is to use a lock (aka mutex/semaphore). Foundation provides the NSLock class family (NSLock, NSRecursiveLock, NSConditionLock, NSDistributedLock) for that purpose. The next code shows a relevant part of a hypothetical NSMutableDictionary subclass, that achieves thread safety with the use of a lock:

@interface SafeMutableDictionary : NSMutableDictionary
   NSLock                *lock_;
   NSMutableDictionary   *internalDictionary_;

// init/dealloc for lock and dictionary omitted...

- (void) setObject:(id) object
            forKey:(id) key
   NSException   *exception;

   exception = nil;
   [lock_ lock];
   [internalDictionary_ setObject:object
   exception = localException;
   [lock_ unlock];
   [exception raise];

// would need to code this for objectForKey: as well

Noteworthy about this code is, that there are two calls to the lock and that we need to catch exceptions if they are raised in the critical section, enclosed by the NSLock calls. It could be disastrous to not catch the exception, because then the lock would remain locked and the application would deadlock the next time the method would be called (1). We can optimize a little using IMPs (see previous articles).

This doesn't just look slow, it turns out to be slow; as running a test case on this shows. In the test case I used, the thread safe code ran in 9.2 s whereas the regular code took 5.2 s. (Without the NS_DURING the test code clocked in at 7.0 s. Using IMPs and no NS_DURING 6.8 s).

So routines, that need to be thread safe and are called often, can prove to be a burden on your performance!

You can download my test code. Since this is ProjectBuilderWO project you will have to import it into the new Project Builder, if you don't have ProjectBuilderWO installed. You can install ProjectBuilderWO from the developer CD as a custom install option.

If you want to discuss this articles, please do so in this thread in the Mull e kybernetiK Optimization Forum.
(1)In this case, an exception will only be raised if object is nil. So that exception could be avoided by checking before hand and the NS_DURING could be left out.