This article republished with permission from CocoaWithLove.com. Copyright Matt Gallagher, all rights reserved.
The Foundation framework provides all the tools you need for inter-thread communication — without needing to handling locks and synchronization yourself. I'll show you Cocoa's tools for inter-thread communication, notifications and easy synchronization — including far simpler code for posting NSNotifications on the main thread than the Cocoa documentation suggests.
Introduction
Multi-threaded code has a reputation for being difficult to write and prone to deadlocks, race conditions and unpredictable behaviors.While this remains true if you are forced to handle locking yourself, Foundation provides all the tools you require to manage typical multi-threaded situations so that you can avoid the risks of locks entirely.
This post comes from my shock at Apple's suggestion (in their Delivering Notifications To Particular Threads) that something as simple as sending an
NSNotification
from one thread to another should take dozens of lines of code and a dedicated class for the purpose.It isn't that difficult. Sending data between threads (including notifications) is a one line job.
Rules for safe simple threading
Simple thread safety in Cocoa requires just two rules:- Every variable or object must nominally belong to a thread (although may be completely handed over to a different thread) and must not be used in multiple threads without handover (unless it is on the list of explicity thread-safe classes).
- All communication between threads (after thread startup) should use
performSelector:onThread:withObject:waitUntilDone:
and both the receiver and the "object" should belong (or be handed over) to the target thread.
NSRunLoop
. Since communication after construction is normally one-way (from worker thread back to the main thread) this is rarely a significant limitation. Other threads can invoke the [NSRunLoop currentRunLoop]
's runMode:beforeDate
to process the run loop and receive messages.A lot of multi-threaded code does not follow these rules. A lot of multi-threaded code uses a careful system of locks, synchronized sections, volatile variables and atomic operations to allow single objects to be accessed simultaneously from multiple threads. This does work but generally speaking: you don't want to design code this way. It's tricky, confusing and prone to errors. To illustrate, you can have a look at how many changes I had to make to my AudioStreamer code between the first and second versions — rock-solid, manually-locked code is annoying and difficult to write.
Instead, as much as possible, you should write your code to keep all objects compartmentalized to individual threads and to restrict communication between threads to method invocations using
performSelector:onThread:withObject:waitUntilDone:
. To explain how this works, I'll show a simple example of something running in a separate thread and show how it can perform all its inter-thread communication using existing Foundation methods to automatically handle all thread safety issues.Scenario: writing to a network NSFileHandle in a worker thread
One of the simplest situations where you may want multithreading is writing to a network socket'sNSFileHandle
. This is a synchronous operation (blocks until complete) so it is a good idea to perform this in a worker thread so that the main thread of your program can remain responsive.The following class is an
NSOperation
subclass. If you don't know about NSOperation
, it's an object that you can add to an NSOperationQueue
to have the object's main
method run in a separate thread (see Apple's Threading Programming Guide). While NSThread
's detachNewThreadSelector:toTarget:withObject:
is the best way to launch a single worker thread, NSOperation
s are the best way to run a series threaded tasks (or has been since serious bugs in it were fixed in 10.5.7).This class' init method takes an
NSFileHandle
and an NSData
object on construction and writes the data to the file handle in its main
method.@interface FileWriteOperation : NSOperation { NSFileHandle *fileHandle; NSData *data; } @end @implementation FileWriteOperation - (id)initWithFileHandle:(NSFileHandle *)aFileHandle data:(NSData *)aData { self = [super init]; if (self != nil) { fileHandle = [aFileHandle retain]; data = [aData retain]; } return self; } - (void)main { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; @try { [fileHandle writeData:data]; // At this point, the write has succeeded } @catch (NSException *e) { // At this point, the write has failed } @finally { [pool drain]; } } - (void)dealloc { [fileHandle closeFile]; [fileHandle release]; [data release]; [super dealloc]; } @endThe
NSOperation
is created and its thread is launched as follows:// Assume the operationQueue, fileHandle and fileData already exist [operationQueue addOperation: [[[FileWriteOperation alloc] initWithFileHandle:fileHandle data:fileData] autorelease]];To follow the first rule of thread-safety, after the
FileWriteOperation
is constructed, the fileHandle
value passed into its initWithFileHandle:data:
method cannot be used again outside the NSOperationQueue
's worker thread.Communicating success or failure
The above class works but doesn't have any way of communicating the result. What would be good is if we could send awriteFinishedWithSuccess:YES
at the "write has succeeded
" line and a writeFinishedWithSuccess:NO
at the "write has failed
" line.The unsafe way of doing this is to invoke a method on another object sending the result:
[responseHandler writeFinishedWithSuccess:YES]; // BAD!!If
responseHandle
belongs to another thread, then this method could cause any number of race conditions and other multi-threading issues.The solution though is exceptionally simple:
[responseHandler performSelectorOnMainThread:@selector(writeFinishedWithSuccess:) withObject:[NSNumber numberWithBool:YES] waitUntilDone:NO];This assumes that
responseHandler
is a nominally "main thread" object. You can use the performSelector:onThread:withObject:waitUntilDone:
method and specify a different thread if you need to send the response elsewhere.You'll notice that the parameter has to be an object in this case (
[NSNumber numberWithBool:YES]
instead of simply YES
) and any parameter that you pass to another thread should not be used again in the current thread.Delivering Notifications To Particular Threads
Of course, theFileWriteOperation
class shown above doesn't have a responseHandle
object that it can notify when it is done, so I'd rather use an NSNotification
sent to the NSNotificationCenter
and if any object wants to receive the notification, it can.The
NSNotificationCenter
itself is thread-safe but it delivers the notifications on the thread in which you invoke postNotification:
so if you expect the observers of that notification to belong to a different thread, you've just broken those objects' thread safety.Normally it is a good idea to deliver all notifications on the main thread. We can do this as follows:
// Line of code in some method of some class... [[self class] performSelectorOnMainThread:@selector(postNotification:) withObject: [NSNotification notificationWithName:@"FileWriteOperationSucceeded" object:self]]; + (void)postNotification:(NSNotification *)aNotification { [[NSNotificationCenter defaultCenter] postNofication:aNotification]; }Notice that we don't call
+[NSNotificationCenter defaultCenter]
until we're on the main thread. If we call it on the other thread, it will return the notification center for that other thread.Some classes follow a different behavior of delivering notifications to the thread on which they were constructed (not necessarily the main thread). To follow this behavior, simply save the
[NSThread currentThread]
on construction and perform the postNotification:
selector on that thread.You may have noticed that the notification is passing "
self
" from one thread to another — breaking the thread ownership of "self
". This is only really safe in one of the following cases:- Handover — the parameter will never be used on this thread again.
In this case, if "self
" is complete on the worker thread (for example at the bottom of themain
method shown above). In this case, the object is passing itself back to the other thread. - Thread-safe — if the class or specific methods are guaranteed to be thread-safe.
In this case, if the only methods invoked onself
areretain
,release
and the default pointer comparisonisEqual:
method (these are the methods invoked when removing from anNSMutableArray
). These are guaranteed thread-safe methods.
self
(or any other parameter) safely between threads.Conclusion
The purpose of this article is to communicate two ideas:- Carefully locking and synchronizing makes programming hard but if you design your objects to be used on only one thread at a time, then they are thread-safe without the need for locks or synchronzation (if needed, split your objects into components for separate threads).
- Use inter-thread communication to keep objects in separate threads up-to-date with each other. In Cocoa, this is very simple but you do need to ensure that all parameters you pass are either thread-safe objects or handover objects.
Of couse, there are always situations where you can't run an
NSRunLoop
to process performSelector:onThread:withObject:waitUntilDone:
messages. There are also situations where you feel that one of your object s must be multi-threaded (rather than single thread exclusive). Either of these cases requires will require a synchronized or locked solution but my recommendation is to try to find an NSRunLoop
and thread exclusive solution first as these are significantly easier to manage and prone to fewer potential problems.
excellent well explained.
ReplyDeleteexcellent well explained.
ReplyDeleteDid you know that that you can earn cash by locking special pages of your blog / website?
ReplyDeleteSimply join AdWorkMedia and run their content locking tool.