Mac OS X (Cocoa) NSLock — Simple Explanation of What It Is and Why You Need It
©2006 Darel Rex Finley.  This complete article, unmodified, may be freely distributed for educational purposes.



To understand why you need NSLock, study this little example:


int  x=0, y=0 ;   //  (globals used by all threads)



//  Spawn this method as a thread (via detachNewThreadSelector)
//  several times, to get multiple threads running.


- (void) renderPixels:(id) dummyObj {

  int  a, b ;

  while (YES) {

    //  Get the coordinates of a pixel that needs rendering, and
    //  advance x,y so another thread will pick up the next pixel.

    a=x; b=y; x++;
    if (x==640) {
      x=0; y++; }

    //  Terminate this thread if there are no more pixels to render.
    if (b>=480) return;

    //  Render one pixel.
    ...complex code to render the pixel a,b... }}


This won’t work!  Why not?  Because the middle section (“Get the coordinates...”) will cause conflicts between threads.

For example, what happens if one thread does x++, incrementing x from 639 to 640, and immediately after that another thread does a=x?  The value 640 would be stored in that thread’s a, which is never supposed to happen.

Or, what if one thread does x++, incrementing x from 50 to 51, and immediately after that another thread also does x++, incrementing x from 51 to 52?  Both threads could render the same pixel (the one at x-position 50), and the pixel at x-position 51 would not be rendered at all.

All kinds of horrible things can happen when multiple threads try to use the same global data — even though that data was intended to be used by multiple threads.  NSLock provides a nice way to prevent these problems:  Simply insert the yellow statements as shown here:


int  x=0, y=0 ;   //  (globals used by all threads)

NSLock  *theLock=[NSLock new] ;



//  Spawn this method as a thread (via detachNewThreadSelector)
//  several times, to get multiple threads running.


- (void) renderPixels:(id) dummyObj {

  int  a, b ;

  while (YES) {

    //  Get the coordinates of a pixel that needs rendering, and
    //  advance x,y so another thread will pick up the next pixel.

    [theLock lock];
    a=x; b=y; x++;
    if (x==640) {
      x=0; y++; }
    [theLock unlock];

    //  Terminate this thread if there are no more pixels to render.
    if (b>=480) return;

    //  Render one pixel.
    ...complex code to render the pixel a,b... }}


Wow, can it really be that easy?  Yes it can.  Putting “lock” and “unlock” around the piece of code that deals with x and y protects it from conflicts.  Now, only one thread can be executing that piece of code at any given time.  If another thread wants to execute it too, it must wait until the first thread is finished (with the piece of protected code).

That’s it — that’s all you really need to know to make great use of NSLock.  But if you’re curious about what those lock/unlock statements are really doing, read on:

theLock has two states: locked or unlocked.
 
If a thread tries to lock theLock, and it’s unlocked, then it becomes locked, and that thread continues executing normally.
 
If a thread tries to lock theLock, but it’s already locked, then that thread is suspended, and that thread is added to some sort of list of threads that are waiting on this particular NSLock object.
 
If a thread tries to unlock theLock, but it’s already unlocked, undesirable things may happen.  (If you used lock and unlock properly, as in the above example, this situation will never occur.)
 
If a thread tries to unlock theLock, and it’s locked, and its list of waiting threads is empty, then theLock will become unlocked, and the thread that unlocked it will continue executing.
 
If a thread tries to unlock theLock, and it’s locked, and its list of waiting threads is not empty, then theLock will stay locked, but one of the threads in its list (presumably the one that has been waiting longest) will be removed from the list and reactivated for normal execution.  (Also, the thread that just tried to unlock theLock will continue executing.)
 
While a thread is suspended, waiting on an NSLock, it isn’t chewing up any processor time or occupying a processor core — it’s truly suspended, to be reactivated later by the NSLock object that it tried to lock.

Question:  What if theLock is currently unlocked, and two threads try to lock it at the same time?  Won’t that create the same kind of conflict that we were trying to avoid in the first place?  Indeed, in theory it could.  There’s some seriously spooky magic going on in NSLock’s “lock” method, and my guess is that it involves a special, hardware-based conflict resolution technique.  Whatever it is, you probably wouldn’t want to deal with it in your own code, and it might even change substantially in future versions of the computer, so it’s best left to the OS to handle inside NSLock!

(Update:  Since writing this page, I’ve found that conflict resolution can be performed with a special, atomic instruction that reads and writes a value “simultaneously” — i.e. without any possibility of another thread or processor accessing that value inbetween the read and the write.  Read more about it on the CocoaDev NSLock discussion.)

Another question:  What if my thread method is performing just a single increment or decrement to a global, and that’s all it does?  (e.g. count++;)  I don’t need to put lock/unlock around that, do I?  If two processors try to increment count at the same time, it will just get incremented twice, with the memory chips moderating access, right?

No, unfortunately, you can’t assume that.  What if your count++ gets compiled into processor-executable code that works something like this:

LOAD REG_A WITH [count]
INCR REG_A
STOR REG_A TO   [count]

In that case, two processors could load the same value from count (say, 100), increment it (to 101), and store it back.  The value in count would be only 101, even though two threads tried to increment it with a starting value of 100.  So all code that changes global data must be protected.

Yet another question:  What if part of my thread method reads some global data, but doesn’t write it?  That doesn’t need to be protected with lock/unlock, does it?  The answer is that the access doesn’t need to be protected if there is no possibility of the data being changed while the app is running in a multithreaded state.  Look at the original example where the protected code changed the values of x and y.  Notice that the code that reads the values of x and y (copying them to a and b) is also in the protected area.  That’s vital, because if you read the values of x and y in an unprotected section of code, another thread might be changing them at that time, so they might contain invalid values, or valid values that this thread shouldn’t be using.

Think very carefully about whether global data access should be protected, and when in doubt, protect it!  Also be aware that all sections of code that read or write x and y must be protected by the same NSLock object (theLock, in this case) — otherwise the protection is an illusion!


Note:  If you’re using multithreading in your app, you’ll probably need to know about obtaining the processor count, so you will know the optimal number of threads to create.  For the above example, I would recommend creating the same number of pixel-rendering threads as you have processors.  So if you detect four processors, create four rendering threads.  That means your app will actually have five threads: the four rendering threads plus the main thread that handles user events, creation of rendering threads, etc.  That’s OK, because your main thread will usually be idle anyway.


Warning:  It’s a generally good idea not to lock two locks at a time in the same thread.  Why not?

Suppose you have two locks, A and B.  And suppose thread 1 locks A, while thread 2 locks B.  Then, thread 1 tries to lock B, while thread 2 tries to lock A.  What will happen?  Thread 1 will go to sleep, to be woken up when B is unlocked.  Thread 2 will go to sleep, to be woken up when A is unlocked.  Both threads are suspended, and both locks are locked.  Nothing further happens.  Any other threads that try to lock A or B will also go to sleep and stay asleep.  Your app is now hosed!


Feel free to send me an e-mail about this page!  I’m no Cocoa expert, and could probably use any advice you’d care to throw my way.

Does the brace style in the above code samples freak you out?  Click here to see it explained in a new window.



Back to Tutorials.