|
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:
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:
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:
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:
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. 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.
|