As a courtesy, this is a full free rendering of my book, Programming iOS 6, by Matt Neuburg. Copyright 2013 Matt Neuburg. Please note that this edition is outdated; the current books are iOS 13 Programming Fundamentals with Swift and Programming iOS 13. If my work has been of help to you, please consider purchasing one or both of them, or you can reward me through PayPal at http://www.paypal.me/mattneub. Thank you!
A thread is, simply put, a subprocess of your app that can execute even while other subprocesses are also executing. Such simultaneous execution is called concurrency. The iOS frameworks use threads all the time; if they didn’t, your app would be less responsive to the user — perhaps even completely unresponsive. The genius of the frameworks, though, is that, for the most part, they use threads precisely so that you don’t have to.
For example, suppose your app is downloading something from the network (Chapter 37). This download doesn’t happen all by itself; somewhere, someone is running code that interacts with the network and obtains data. Similarly, how does Core Motion work (Chapter 35)? The data from the sensors is being gathered and processed constantly, with extra calculations to separate gravity from user-induced acceleration and to account for bias and scaling in the gyroscope. Yet none of that prevents your code from running; none of that prevents the user from tapping and swiping things in your interface. That’s concurrency in action.
It is a testament to the ingenuity of the iOS frameworks that this book has proceeded so far with so little mention of threads. Indeed, it would have been nice to avoid the topic altogether. Threads are difficult and dangerous, and if at all possible you should avoid them. But sometimes that isn’t possible. So this chapter introduces threads, along with a warning: threads entail complications and subtle pitfalls, and can make your code hard to debug. There is much more to threading, and especially to making your threaded code safe, than this chapter can possibly touch on. For detailed information about the topics introduced in this chapter, read Apple’s Concurrency Programming Guide and Threading Programming Guide.
You are always using some thread. All your code must run somewhere; “somewhere” means a thread. When code calls a method, that method normally runs on the same thread as the code that called it. Your code is called through events (Chapter 11); those events normally call your code on the main thread. The main thread has certain special properties:
The main thread thus has a very great deal of work to do. Here’s how life goes in your app:
The bottleneck here is obviously step 2, the running of your code. Your code runs on the main thread. That means the main thread can’t do anything else while your code is running. No events can arrive while your code is running. The user can’t interact with the interface while your code is running. But this is usually no problem, because:
On the other hand, as I’ve already mentioned, the frameworks operate in secondary threads all the time. The reason this doesn’t affect you is that they usually talk to your code on the main thread. You have seen many examples of this in the preceding chapters. For example:
Thus, you can (and should) usually ignore threads and just keep plugging away on the main thread. However, there are two kinds of situation in which your code will need to be explicitly aware of threading issues:
Some frameworks explicitly inform you in their documentation that callbacks are not guaranteed to take place on the main thread. For example, the documentation on CATiledLayer (Chapter 20) warns that drawLayer:inContext:
is called in a background thread. By implication, our drawRect:
code, triggered by CATiledLayer to update tiles, is running in a background thread. Fortunately, the UIKit drawing-related classes are thread-safe, and so is accessing the current context. Nevertheless, we cannot completely ignore the fact that this code is not running on the main thread.
Similarly, the documentation on AV Foundation (Chapter 28, Chapter 30) warns that its blocks and notifications can arrive on a background thread. So if you intend to update the user interface, or use a value that might also be used by your main-thread code, you’ll need to be thread-conscious.
If your code takes significant time to run, you might need to run that code on a background thread, rather than letting it block the main thread and prevent anything else from happening there. For example:
URLForUbiquityContainerIdentifier:
during app launch. The documentation told me to call this method on a background thread, because it can take some time to return; we don’t want to block the main thread waiting for it, because the app is trying to launch on the main thread, and the user won’t see our interface until the launch process is over.
enumerateEventsMatchingPredicate:
on a background thread in order to prevent the user interface from freezing up in case the enumeration took a long time. If I hadn’t done this, then when the user taps the button that triggers this call, the button will stay highlighted for a significant amount of time, during which the interface will be completely frozen.
Similarly, when your app is in the process of being suspended into the background, or resumed from the background, your app should not block the main thread for too long; it must act quickly and get out of the way. This isn’t just a matter of aesthetics or politeness; the system “watchdog” will summarily kill your app if it discovers that the main thread is blocked for too long.
The one certain thing about computer code is that it just clunks along the path of execution, one statement at a time. Lines of code, in effect, are performed in the order in which they appear. With threading, that certainty goes right out the window. If you have code that can be performed on a background thread, then you don’t know when it will be performed in relation to the code being performed on any other thread. For example, any line of your background-thread code could be interleaved between any two lines of your main-thread code.
You also might not know how many times a piece of your background-thread code might be running simultaneously. Unless you take steps to prevent it, the same code could be spawned off as a thread even while it’s already running in a thread. So any line of your background-thread code could be interleaved between any two lines of itself.
This situation is particularly threatening with regard to shared data. Suppose two threads were to get hold of the same object and change it. Who knows what horrors might result? Objects in general have state, adding up to the state of your app as a whole. If multiple threads are permitted to access your objects, they and your entire app can be put into an indeterminate or nonsensical state.
This problem cannot be solved by simple logic. For example, suppose you try to make data access safe with a condition, as in this pseudo-code:
if (no other thread is touching this data) do something to the data...
Such logic cannot succeed. Suppose the condition succeeds; no other thread is touching this data. But between the time when that condition is evaluated and the time when the next line executes and you start to do something to the data, another thread can come along and start touching the data!
It is possible to request assistance at a deeper level to ensure that a section of code is not run by two threads simultaneously. For example, you can implement a lock around a section of code. But locks generate an entirely new level of potential pitfalls. In general, a lock is an invitation to forget to use the lock, or to forget to remove the lock after you’ve set it. And threads can end up contending for a lock in a way that permits neither thread to proceed.
Another problem is that the lifetime of a thread is independent of the lifetimes of other objects in your app. When an object is about to go out of existence and its dealloc
has been called and executed, you are guaranteed that none of your code in that object will ever run again. But a thread might still be running, and might try to talk to your object, even after your object has gone out of existence. You cannot solve this problem by having the thread retain your object, because then there is the danger that the thread might be the last code retaining your object, so that when the thread releases your object, your object’s dealloc
is called on that thread rather than the main thread, which could be a disaster.
Not only is threaded code hard to get right; it’s also hard to test and hard to debug. It introduces indeterminacy, so you can easily make a mistake that never appears in your testing, but that does appear for some user. The real danger is that the user’s experience will consist only of distant consequences of your mistake, long after the point where you made it, making the real cause of the problem extraordinarily difficult to track down.
Perhaps you think I’m trying to scare you away from using threads. You’re right! For an excellent (and suitably frightening) account of some of the dangers and considerations that threading involves, see Apple’s tech note TN2109. If terms like race condition and deadlock don’t strike fear into your veins, look them up on Wikipedia.
When you call NSLog in your multithreaded code, the output in the console displays a number (in square brackets, after the colon) identifying the thread on which it was called. This is unbelievably helpful.
Without pretending to completeness or even safety, this section will illustrate three approaches to threading, progressing from worst to best. To give the examples a common base, we envision an app that draws the Mandelbrot set. (The actual code, not all of which is shown here, is adapted from a small open source project I downloaded from the Internet.) All it does is draw the basic Mandelbrot set in black and white, but that’s enough crunching of numbers to introduce a significant delay. The idea is then to see how we can get that delay off the main thread.
The app contains a UIView subclass, MyMandelbrotView, which has one instance variable, a CGContextRef called _bitmapContext
. Here’s the structure of MyMandelbrotView’s implementation:
// jumping-off point: draw the Mandelbrot set - (void) drawThatPuppy { [self makeBitmapContext: self.bounds.size]; CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); [self drawAtCenter: center zoom: 1]; [self setNeedsDisplay]; } // create (and memory manage) instance variable - (void) makeBitmapContext:(CGSize)size { if (self->_bitmapContext) CGContextRelease(self->_bitmapContext); // ... configure arguments ... CGContextRef context = CGBitmapContextCreate(nil, /* ... */); self->_bitmapContext = context; } // draw pixels of self->_bitmapContext - (void) drawAtCenter:(CGPoint)center zoom:(CGFloat)zoom { // .... do stuff to self->_bitmapContext } // turn pixels of self->_bitmapContext into CGImage, draw into ourselves - (void) drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGImageRef im = CGBitmapContextCreateImage(self->_bitmapContext); CGContextDrawImage(context, self.bounds, im); CGImageRelease(im); } // final memory managment - (void) dealloc { if (self->_bitmapContext) CGContextRelease(self->_bitmapContext); }
(I haven’t discussed creating a bitmap context from scratch; see “Graphics Contexts” in the Quartz 2D Programming Guide for example code. In this case, we take advantage of a feature that lets us pass nil as the first argument to CGBitmapContextCreate
, which relieves us of the responsibility for creating and memory-managing a data buffer associated with the graphics context.)
The drawAtCenter:zoom:
method, which calculates the pixels of the instance variable _bitmapContext
, is time-consuming, and we can see this by running the app on a device. If the entire process is kicked off by tapping a button whose action handler calls drawThatPuppy
, there is a significant delay before the Mandelbrot graphic appears in the interface, during which time the button remains highlighted. That is a sure sign that we are blocking the main thread. We will consider three ways of moving this work off onto a background thread: with an old-fashioned manual thread, with NSOperation, and with Grand Central Dispatch.
The simple way to create a thread manually is to send performSelectorInBackground:withObject:
to some object containing a method to be performed on a background thread. Even with this simple approach, there is additional work to do:
performSelectorInBackground:withObject:
can take only one parameter, whose value you supply as the second argument. So, if you want to pass more than one piece of information into the thread, or if the information you want to pass isn’t an object, you’ll need to pack it into a single object. Typically, this will be an NSDictionary.
We’ll rewrite MyMandelbrotView to use manual threading. Our drawAtCenter:zoom:
method takes two parameters (and neither is an object), so we’ll have to pack the argument that we pass into the thread, as a dictionary. Once inside the thread, we’ll set up our autorelease pool and unpack the dictionary. This will all be made much easier if we interpose a trampoline method between drawThatPuppy
and drawAtCenter:zoom:
. So our implementation now looks like this (ignoring the parts that haven’t changed):
- (void) drawThatPuppy { [self makeBitmapContext: self.bounds.size]; CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); NSDictionary* d = @{@"center": [NSValue valueWithCGPoint:center], @"zoom": @1}; [self performSelectorInBackground:@selector(reallyDraw:) withObject:d]; // [self setNeedsDisplay]; } // trampoline, background thread entry point - (void) reallyDraw: (NSDictionary*) d { @autoreleasepool { [self drawAtCenter: [d[@"center"] CGPointValue] zoom: [d[@"zoom"] intValue]]; } }
So far so good, but we haven’t yet figured out how to draw our view. We have commented out the call to setNeedsDisplay
in drawThatPuppy
, because it’s too soon; the call to performSelectorInBackground:withObject:
launches the thread and returns immediately, so our _bitmapContext
instance variable isn’t ready yet. Clearly, we need to call setNeedsDisplay
after drawAtCenter:zoom:
finishes generating the pixels of the graphics context. We can do this at the end of our trampoline method reallyDraw:
, but we must remember that we’re now in a background thread. Because setNeedsDisplay
is a form of communication with the interface, we should call it on the main thread. We can do that with easily with performSelectorOnMainThread:withObject:waitUntilDone:
. For maximum flexibility, it will probably be best to implement a second trampoline method:
// trampoline, background thread entry point - (void) reallyDraw: (NSDictionary*) d { @autoreleasepool { [self drawAtCenter: [[d objectForKey:@"center"] CGPointValue] zoom: [[d objectForKey:@"zoom"] intValue]]; [self performSelectorOnMainThread:@selector(allDone) withObject:nil waitUntilDone:NO]; } } // called on main thread! background thread exit point - (void) allDone { [self setNeedsDisplay]; }
This code is specious; the seeds of nightmare are already sown. We now have a single object, MyMandelbrotView, some of whose methods are to be called on the main thread and some on a background thread; this invites us to become confused at some later time. Even worse, the main thread and the background thread are constantly sharing a piece of data, the instance variable _bitmapContext
;
what’s to stop some other code from coming along and triggering drawRect:
while drawAtCenter:zoom:
is in the middle of filling _bitmapContext
?
To solve these problems, we might need to use locks, and we would probably have to manage the thread more explicitly. For instance, we might use the NSThread class, which lets us retain our thread as an instance and query it from outside (with isExecuting
and similar). Such code can become quite elaborate and difficult to understand, even with an extremely basic implementation. It will be easier and safer at this point to use NSOperation, the subject of the next threading approach.
The essence of NSOperation is that it encapsulates a task, not a thread. The operation described by an NSOperation object may be performed on a background thread, but you don’t have to concern yourself with that directly. You describe the operation and add the NSOperation to an NSOperationQueue to set it going. When the operation finishes, you are notified, typically by the NSOperation posting a notification. You can query both the queue and its operations from outside with regard to their state.
We’ll rewrite MyMandelbrotView to use NSOperation. We need a property, an NSOperationQueue; we’ll call it queue
. And we have a new class, MyMandelbrotOperation, an NSOperation subclass. It is possible to take advantage of a built-in NSOperation subclass such as NSInvocationOperation or NSBlockOperation, but I’m deliberately illustrating the more general case by subclassing NSOperation itself.
Our implementation of drawThatPuppy
makes sure that the queue exists; it then creates an instance of MyMandelbrotOperation, configures it, registers for its notification, and adds it to the queue:
- (void) drawThatPuppy { CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); if (!self.queue) { NSOperationQueue* q = [NSOperationQueue new]; self.queue = q; // retain policy } MyMandelbrotOperation* op = [[MyMandelbrotOperation alloc] initWithSize:self.bounds.size center:center zoom:1]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(operationFinished:) name:@"MyMandelbrotOperationFinished" object:op]; [self.queue addOperation:op]; }
Our time-consuming calculations are performed by MyMandelbrotOperation. An NSOperation subclass, such as MyMandelbrotOperation, will typically have at least two methods:
main
method
MyMandelbrotOperation has three instance variables for configuration (_size
, _center
, and _zoom
), to be set in its initializer; it must be told MyMandelbrotView’s geometry explicitly because it is completely separate from MyMandelbrotView. MyMandelbrotOperation also has its own CGContextRef instance variable, _bitmapContext
,
along with an accessor so MyMandelbrotView can retrieve a reference to this graphics context when the operation has finished. Note that this is different from MyMandelbrotView’s _bitmapContext
; one of the benefits of using NSOperation is that we are no longer sharing data so promiscuously between threads.
Here’s the implementation for MyMandelbrotOperation. All the calculation work has been transferred from MyMandelbrotView to MyMandelbrotOperation without change; the only difference is that _bitmapContext
now means MyMandelbrotOperation’s instance variable:
- (id) initWithSize: (CGSize) sz center: (CGPoint) c zoom: (CGFloat) z { self = [super init]; if (self) { self->_size = sz; self->_center = c; self->_zoom = z; } return self; } - (void) dealloc { if (self->_bitmapContext) CGContextRelease(self->_bitmapContext); } - (CGContextRef) bitmapContext { return self->_bitmapContext; } - (void)makeBitmapContext:(CGSize)size { // ... same as before ... } - (void)drawAtCenter:(CGPoint)center zoom:(CGFloat)zoom { // ... same as before ... } - (void) main { if ([self isCancelled]) return; [self makeBitmapContext: self->_size]; [self drawAtCenter: self->_center zoom: self->_zoom]; if (![self isCancelled]) [[NSNotificationCenter defaultCenter] postNotificationName:@"MyMandelbrotOperationFinished" object:self]; }
The only method of interest is main
. First, we call the NSOperation method isCancelled
to make sure we haven’t been cancelled while sitting in the queue; this is good practice. Then, we do exactly what drawThatPuppy
used to do, initializing our graphics context and drawing into its pixels.
When the operation is over, we need to notify MyMandelbrotView to come and fetch our data. There are two ways to do this; either main
can post a notification through the NSNotificationCenter, or MyMandelbrotView can use key–value observing (Chapter 13) to be notified when our isFinished
key path changes. We’ve chosen the former approach; observe that we check one more time to make sure we haven’t been cancelled.
Now we are back in MyMandelbrotView, hearing that MyMandelbrotOperation has finished. We must immediately pick up any required data, because the NSOperationQueue is about to release this NSOperation. However, we must be careful; the notification may have been posted on a background thread, in which case our method for responding to it will also be called on a background thread. We are about to set our own graphics context and tell ourselves to redraw; those are things we want to do on the main thread. So we immediately trampoline ourselves out to the main thread:
// warning! called on background thread - (void) operationFinished: (NSNotification*) n { [self performSelectorOnMainThread:@selector(redrawWithOperation:) withObject:[n object] waitUntilDone:NO]; }
As we set MyMandelbrotView’s _bitmapContext
by reading MyMandelbrotOperation’s _bitmapContext
, we must concern ourselves with the memory management of a CGContext obtained from an object that may be about to release that context:
// now we're back on the main thread - (void) redrawWithOperation: (MyMandelbrotOperation*) op { [[NSNotificationCenter defaultCenter] removeObserver:self name:@"MyMandelbrotOperationFinished" object:op]; CGContextRef context = [op bitmapContext]; if (self->_bitmapContext) CGContextRelease(self->_bitmapContext); self->_bitmapContext = (CGContextRef) context; CGContextRetain(self->_bitmapContext); [self setNeedsDisplay]; }
Using NSOperation instead of manual threading may not seem like any reduction in work, but it is a tremendous reduction in headaches:
operationFinished:
, and that’s a method we’d never call explicitly ourselves, so we won’t misuse it accidentally.
_bitmapContext
. The only moment of data sharing comes in redrawWithOperation:
, when we must set MyMandelbrotView’s _bitmapContext
to MyMandelbrotOperation’s _bitmapContext
.
Even if multiple MyMandelbrotOperation objects are added to the queue, the moments when we set MyMandelbrotView’s _bitmapContext
all occur on the main thread, so they cannot conflict with one another.
The coherence of MyMandelbrotView’s _bitmapContext
does depend upon our obedience to an implicit contract not to set it or write into it anywhere except a few specific moments in MyMandelbrotView’s code. But this is always a problem with data sharing in a multithreaded world, and we have done all we can to simplify the situation.
If we are concerned with the possibility that more than one instance of MyMandelbrotOperation might be added to the queue and executed concurrently, we have a further defense — we can set the NSOperationQueue’s maximum concurrency level to 1:
NSOperationQueue* q = [NSOperationQueue new]; [q setMaxConcurrentOperationCount:1]; self.queue = q;
This turns the NSOperationQueue into a true serial queue; every operation on the queue must be completely executed before the next can begin. This might cause an operation added to the queue to take longer to execute, if it must wait for another operation to finish before it can even get started; however, this delay might not be important. What is important is that by executing the operations on this queue completely separately, we guarantee that only one operation at a time can do any data sharing. A serial queue is thus a form of data locking.
Because MyMandelbrotView can be destroyed (if, for example, its view controller is destroyed), there is still a risk that it will create an operation that will outlive it and will try to access it after it has been destroyed. We can reduce that risk by canceling all operations in our queue before releasing it:
- (void)dealloc { // release the bitmap context if (self->_bitmapContext) CGContextRelease(self->_bitmapContext); [self->_queue cancelAllOperations]; }
In our code, we are still using the potentially confusing trampoline technique. Our operationFinished:
method is called by a notification on what may be a background thread, so it calls redrawWithOperation:
on the main thread. By a neat trick involving a block, we can actually eliminate the trampoline and both of those methods.
Recall, from Chapter 11, the NSNotificationCenter method addObserverForName:object:queue:usingBlock:
. The queue:
argument here is an NSOperationQueue — the queue on which we’d like our block to be called. I said in Chapter 11 that this will usually be nil, signifying the same thread that posted the notification, which will usually be the main thread. In this case, though, the thread that posted the notification might not be the main thread, so we can request explicitly that the block be called on the main thread. In other words, NSNotificationCenter will perform the trampolining for us.
As I said in Chapter 12, we have to take precautions to avoid a retain cycle; addObserverForName:object:queue:usingBlock:
returns an observer which retains us, so we mustn’t retain it in turn. We don’t want to keep our observer as an instance variable because there might be multiple conflicting simultaneous observers. So we declare it as __weak
to prevent the retain cycle, and we declare it as __block
so that we can see its future value (the value it will have after the call to addObserverForName:object:queue:usingBlock:
returns) inside the block and use it to deregister when the notification arrives:
MyMandelbrotOperation* op = [[MyMandelbrotOperation alloc] initWithSize:self.bounds.size center:center zoom:1]; __block __weak id observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"MyMandelbrotOperationFinished" object:op queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { MyMandelbrotOperation* op2 = note.object; CGContextRef context = [op2 bitmapContext]; if (self->_bitmapContext) CGContextRelease(self->_bitmapContext); self->_bitmapContext = (CGContextRef) context; CGContextRetain(self->_bitmapContext); [self setNeedsDisplay]; [[NSNotificationCenter defaultCenter] removeObserver:observer name:@"MyMandelbrotOperationFinished" object:op2]; }]; [self.queue addOperation:op];
That’s pretty elegant, but in the next section we’ll go even further — we’ll effectively eliminate data sharing entirely by using Grand Central Dispatch.
A number of useful methods mentioned earlier in this book expect an NSOperationQueue argument; see Chapter 35 (startDeviceMotionUpdatesToQueue:withHandler:
, and similarly for the other sensors) and Chapter 37 (sendAsynchronousRequest:queue:completionHandler:
).
Grand Central Dispatch, or GCD, is a sort of low-level analogue to NSOperation and NSOperationQueue (in fact, NSOperationQueue uses GCD under the hood). When I say GCD is low-level, I’m not kidding; it’s effectively baked into the operating system kernel. Thus it can be used by any code whatsoever and is tremendously efficient.
Using GCD is like a mixture of the manual threading approach with the NSOperationQueue approach. It’s like the manual threading approach because code to be executed on one thread appears together with code to be executed on another; however, you have a much better chance of keeping the threads and data management straight, because GCD uses Objective-C blocks. It’s like the NSOperationQueue approach because it uses queues; you express a task and add it to a queue, and the task is executed on a thread as needed. Moreover, by default these queues are serial queues, with each task on a queue finishing before the next is started, which, as we’ve already seen, is a form of data locking.
We’ll rewrite MyMandelbrotView to use GCD. The structure of its interface is very slightly changed from the original, nonthreaded version. Our makeBitmapContext:
method now returns a graphics context rather than setting an instance variable directly; and our drawAtCenter:zoom:
method now takes an additional parameter, the graphics context to draw into. Also, we have a new instance variable to hold our queue, which is a dispatch queue; a dispatch queue is a lightweight opaque pseudo-object consisting essentially of a list of blocks to be executed:
@implementation MyMandelbrotView { CGContextRef _bitmapContext; dispatch_queue_t _draw_queue; }
In MyMandelbrotView’s implementation, we create our dispatch queue as the view is created:
- (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder: aDecoder]; if (self) { self->_draw_queue = dispatch_queue_create("com.neuburg.mandeldraw", nil); } return self; }
A call to dispatch_queue_create
must be balanced by a call to dispatch_release
. New in iOS 6, however, ARC understands GCD pseudo-objects and will take care of this for us (and in fact calling dispatch_release
explicitly is forbidden).
Now for the implementation of drawThatPuppy
. Here it is:
- (void) drawThatPuppy { CGPoint center = ❶ CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); dispatch_async(self->_draw_queue, ^{ ❷ CGContextRef bitmap = [self makeBitmapContext: self.bounds.size]; [self drawAtCenter: center zoom: 1 context:bitmap]; dispatch_async(dispatch_get_main_queue(), ^{ ❸ if (self->_bitmapContext) CGContextRelease(self->_bitmapContext); self->_bitmapContext = bitmap; [self setNeedsDisplay]; }); }); }
That’s all there is to it. No trampoline methods. No performSelector...
. No packing arguments into a dictionary. No autorelease pools. No instance variables. No notifications. And effectively no sharing of data across threads. That’s the beauty of blocks.
We begin by calculating our |
|
Now comes our task to be performed in a background thread on our queue, |
|
Now we need to get back onto the main thread. How do we do that? With |
I used this same technique in Chapter 36, to call URLForUbiquityContainerIdentifier:
on a background thread (so as not to block the main thread and the app launch process) and then to set an instance variable on the main thread (so as not to conflict with any other access to that instance variable).
The benefits and elegance of GCD as a form of concurrency management are stunning. The bitmap
variable is not shared; it is local to each specific call to drawThatPuppy
. The nested blocks are executed in succession, so any instance of bitmap
must be completely filled with pixels before being used to set the _bitmapContext
instance variable. Moreover, the entire operation is performed on a serial queue, and _bitmapContext
is touched only from code running on the main thread; thus there is no data sharing and no possibility of conflict. Our code is also highly maintainable, because the entire task on all threads is expressed within the single drawThatPuppy
method, thanks to the use of blocks; indeed, the code is only very slightly modified from the original, nonthreaded version.
You might object that we still have methods makeBitmapContext:
and drawAtCenter:zoom:context:
hanging around MyMandelbrotView, and that we must therefore still be careful not to call them on the main thread, or indeed from anywhere except from within drawThatPuppy
. If that were true, we could at this point destroy makeBitmapContext:
and drawAtCenter:zoom:context:
and move their functionality completely into drawThatPuppy
. But it isn’t true, because these methods are now thread-safe: they are self-contained utilities that touch no instance variables or persistent objects, so it doesn’t matter what thread they are called on. Still, I’ll demonstrate in a moment how we can intercept an accidental attempt to call a method on the wrong thread.
The two most important GCD functions are:
dispatch_async
Push a block onto the end of a queue for later execution, and proceed immediately with our own code. Thus, we can finish our own execution without waiting for the block to execute.
Examples of using dispatch_async
as a way of getting back onto the main thread (dispatch_get_main_queue
) in order to talk to the interface from inside a block that might be executed on a background thread have appeared in Chapter 29 and Chapter 30. Also, in Chapter 11 I described a technique for using dispatch_async
to step onto the main thread even though you’re already on the main thread, as a way of waiting for the run loop to complete and for the interface to settle down — a minimal form of delayed performance. I used that technique in Chapter 19, Chapter 21, Chapter 28, and Chapter 36.
dispatch_sync
Push a block onto the end of a queue for later execution, and wait until the block has executed before proceeding with our own code — because, for example, you intend to use a result that the block is to provide. The purpose of the queue would be, once again, as a lightweight, reliable version of a lock, mediating access to a shared resource. Here’s a case in point, from Apple’s own code:
- (AVAsset*)asset { __block AVAsset *theAsset = nil; dispatch_sync(assetQueue, ^(void) { theAsset = [[self getAssetInternal] copy]; }); return theAsset; }
Any thread might call the asset
method; to avoid problems, we require that only blocks run from a particular queue (assetQueue
) may touch an AVAsset. But we need the result that this block returns; hence the call to dispatch_sync
.
Examples in this book have also made use of dispatch_after
(Chapter 11, Chapter 15, Chapter 20, Chapter 23) as an alternative to performSelector:withObject:afterDelay:
.
Another useful GCD function is dispatch_once
, a thread-safe way of assuring that a block is called only once; it’s often used to vend a singleton. I showed an example in Chapter 3.
Besides serial dispatch queues, there are also concurrent dispatch queues. A concurrent queue’s blocks are started in the order in which they were submitted to the queue, but a block is allowed to start while another block is still executing. Obviously, you wouldn’t want to submit to a concurrent queue a task that touches a shared resource — that would be throwing away the entire point of serial queues. The advantage of concurrent queues is a possible speed boost when you don’t care about the order in which multiple tasks are finished — for example, when you want to do something in response to every element of an array. The built-in global queues (available by calling dispatch_get_global_queue
) are concurrent; you can also create a concurrent queue by passing DISPATCH_QUEUE_CONCURRENT
as the second argument to dispatch_queue_create
.
An interesting tweak is that you can queue up a barrier block on a concurrent queue; a barrier block has the property that it won’t be dequeued until all the blocks preceding it on the queue have been not only dequeued but fully executed, and that no blocks following it in the queue will be dequeued until it itself has fully executed (rather like Benjamin Britten’s “curlew sign,” signifying that every musician must wait here until all the other musicians have reached the same point).
A frequent use of concurrent queues is with dispatch_apply
. This function is like dispatch_sync
(the caller pauses until the block has finished executing), but the block is called multiple times with an iterator argument. Thus, dispatch_apply
on a concurrent queue is like a for loop whose iterations are multithreaded; on a device with multiple cores, this could result in a speed improvement. (Of course, this technique is applicable only if the iterations do not depend on one another.)
Arbitrary context data can be attached to a queue in the form of key–value pairs (dispatch_queue_set_specific
) and retrieved by key. The dispatch_queue_get_specific
function retrieves a key’s value for a queue to which we already have a valid reference; dispatch_get_specific
retrieves a key’s value for the current queue, the one in whose thread we are actually running. In fact, dispatch_get_specific
is the only valid way to identify the current queue (a function formerly used for this purpose, dispatch_get_current_queue
, has been shown to be potentially unsafe and is now deprecated).
We can use this technique, for example, to make certain that a method is called only on the correct queue. Recall that in our Mandelbrot-drawing example, we may be concerned that a method such as makeBitmapContext:
might be called on some other queue than the background queue that we created for this purpose. If this is really a worry, we can attach an identifying key–value pair to that queue when we create it. Both key and value should be pointers, so let’s start by defining some static C strings:
static char* QKEY = "label"; static char* QVAL = "com.neuburg.mandeldraw";
We then create the queue like this:
self->_draw_queue = dispatch_queue_create(QVAL, nil); dispatch_queue_set_specific(self->_draw_queue, QKEY, QVAL, nil);
Later, we can examine that identifying key–value pair for the queue on which a particular method is called:
- (CGContextRef) makeBitmapContext:(CGSize)size { NSAssert(dispatch_get_specific(QKEY) == QVAL, @"Wrong thread");
Note that we are comparing the values purely as pointers; the fact that either one of them is a C string is irrelevant. A common device, where threads generated by different instances of the same class need to be distinguished, is to attach to a queue a key–value pair whose value is the instance itself.
When your app is backgrounded and suspended (Chapter 11), a problem arises if your code is running. The system doesn’t want to kill your code while it’s executing; on the other hand, some other app may need to be given the bulk of the device’s resources now. So as your app goes into the background, the system waits a short time for your app to finish doing whatever it may be doing, but it then suspends your app and stops it by force.
This shouldn’t be a problem from your main thread’s point of view, because your app shouldn’t have any time-consuming code on the main thread in the first place; you now know that you can avoid this by using a background thread. On the other hand, it could be a problem for lengthy background operations, including asynchronous tasks performed by the frameworks. You can request time to complete a lengthy task (or at to least abort it yourself, coherently) in case your app is backgrounded, by wrapping it in calls to UIApplication’s beginBackgroundTaskWithExpirationHandler:
and endBackgroundTask:
.
You call beginBackgroundTaskWithExpirationHandler:
to announce that a lengthy task is beginning; it returns an identification number. At the end of your lengthy task, you call endBackgroundTask:
, passing in that same identification number. This tells the application that your lengthy task is over and that, if your app has been backgrounded while the task was in progress, it is now okay to suspend you.
The argument to beginBackgroundTaskWithExpirationHandler:
is a block, but this block does not express the lengthy task. It expresses what you will do if your extra time expires before you finish your lengthy task. At the very least, your expiration handler must call endBackgroundTask:
, just as your lengthy task would have done; otherwise, your app won’t just be suspended — it will be killed.
If your expiration handler block is called, you should make no assumptions about what thread it is running on.
Let’s use MyMandelbrotView, from the preceding section, as an example. Let’s say that if drawThatPuppy
is started, we’d like it to be allowed to finish, even if the app is suspended in the middle of it, so that our _bitmapContext
instance variable is updated as requested. To try to ensure this, we call beginBackgroundTaskWithExpirationHandler:
beforehand and call endBackgroundTask:
at the end of the innermost block:
- (void) drawThatPuppy { CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); __block UIBackgroundTaskIdentifier bti = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler: ^{ [[UIApplication sharedApplication] endBackgroundTask:bti]; }]; if (bti == UIBackgroundTaskInvalid) return; dispatch_async(self->_draw_queue, ^{ CGContextRef bitmap = [self makeBitmapContext: self.bounds.size]; [self drawAtCenter: center zoom: 1 context:bitmap]; dispatch_async(dispatch_get_main_queue(), ^{ if (self->_bitmapContext) CGContextRelease(self->_bitmapContext); self->_bitmapContext = bitmap; [self setNeedsDisplay]; [[UIApplication sharedApplication] endBackgroundTask:bti]; }); }); }
If our app is backgrounded while drawThatPuppy
is in progress, it will (we hope) be given enough time to live that it can run all the way to the end. Thus, the instance variable _bitmapContext
will be updated, and setNeedsDisplay
will be called, before we are actually suspended. Our drawRect:
will not be called until our app is brought back to the front, but there’s nothing wrong with that.
The __block
qualifier on the declaration of bti
is like the __block
qualifier in the addObserverForName:object:queue:usingBlock:
example earlier: it allows us to see, inside the block, the value that bti
will have when the call to beginBackgroundTaskWithExpirationHandler:
returns (Chapter 3). The check against UIBackgroundTaskInvalid
can do no harm, and there may be situations or devices where our request to complete this task in the background will be denied.
It’s good policy to use a similar technique when you’re notified that your app is being backgrounded. You might respond to the app delegate message applicationDidEnterBackground:
(or the corresponding UIApplicationDidEnterBackgroundNotification
) by saving data and reducing memory usage, but this can take time, whereas you’d like to return from applicationDidEnterBackground:
as quickly as possible. A reasonable solution is to implement applicationDidEnterBackground:
very much like drawThatPuppy
in the example I just gave: call beginBackgroundTaskWithExpirationHandler:
and then call dispatch_async
to get off the main thread, and do your saving and so forth in its block.
What about lengthy asynchronous operations such as networking (Chapter 37)? As far as I can tell, it might not strictly be necessary to use beginBackgroundTaskWithExpirationHandler:
with NSURLConnection; it appears that NSURLConnection has the ability to resume automatically after an interruption when your app is suspended. Still, it might be better not to rely on that behavior (or on an assumption that, just because the network is present now, it will be present when the app awakes from suspension), so you might like to integrate beginBackgroundTaskWithExpirationHandler:
into your use of NSURLConnection.
Such integration can be just a little tricky, because beginBackgroundTaskWithExpirationHandler:
and endBackgroundTask:
rely on a shared piece of information, the UIBackgroundTaskIdentifier — but the downloading operation begins in one place (when the NSURLConnection is created, or when it is told to start
) and ends in one of two other places (the NSURLConnection’s delegate is informed that the download has failed or succeeded), so information is not so easily shared. However, with something like our MyDownloader class, an entire single downloading operation is encapsulated, and we can give the class a UIBackgroundTaskIdentifier instance variable. So, we would set this instance variable with a call to beginBackgroundTaskWithExpirationHandler:
just before telling the connection to start
, and then both connection:didFailWithError:
and connectionDidFinishLoading:
would use the value stored in that instance variable to call endBackgroundTask:
as their last action.