iOS5 Cookbook -- Concurrency -- 5.0
Concurrency is achieved when two or more tasks are executed at the same time. Modern operating systems have the ability to run tasks concurrently even on one CPU. They achieve this by giving every task a certain time slice from the CPU. For instance, if there are 10 tasks to be executed in one second all with the same priority, the operating system will divide 1000 miliseconds (there are 1000 miliseconds per second) by 10 (tasks) and will give each task 100 miliseconds of the CPU time. That means all these tasks will then be executed in the same second and they will appear to have been executed con- currently.
However, with advances in technology, now we have CPUs that have more than one core. This means that the CPU is truly capable of executing tasks at the same time. The operating system will dispatch the tasks to the CPU and will wait until they are done. That simple!
Grand Central Dispatch, or GCD for short, is a low-level C API that works with block objects. The real use for GCD is to dispatch tasks to multiple cores without making you, the programmer, worry about which core is executing which task. On Mac OS X, multicore devices, including laptops, have been available to users for quite some time. With the introduction of multicore devices such as the iPad 2, programmers can write amazing multicore-aware multithreaded apps for iOS.
At the heart of GCD are dispatch queues. Dispatch queues, as we will see soon, are pools of threads managed by GCD on the host operating system, whether it is iOS or Mac OS X. You will not be working with these threads directly. You will just work with dispatch queues, dispatching tasks to these queues and asking the queues to invoke your tasks. GCD offers several options for running tasks: synchronously, asynchro- nously, after a certain delay, etc.
To start using GCD in your apps, you don’t have to import any special library into your project. Apple has already incorporated GCD into various frameworks, including Core Foundation and Cocoa/Cocoa Touch. All methods and data types available in GCD start with a dispatch_ keyword. For instance, dispatch_async allows you to dispatch a task on a queue for asynchronous execution, whereas dispatch_after allows you to run a block of code after a given delay.
Traditionally, programmers had to create their own threads to perform tasks in parallel. For instance, an iOS developer would create a thread similar to this to perform an operation 1000 times:
- (void) doCalculation{ /* Do your calculation here */ } - (void) calculationThreadEntry{ @autoreleasepool { NSUInteger counter = 0; while ([[NSThread currentThread] isCancelled] == NO){ [self doCalculation]; counter++; if (counter >= 1000){ break; } } } } - (BOOL) application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{ /* Start the thread */ [NSThread detachNewThreadSelector:@selector(calculationThreadEntry) toTarget:self withObject:nil]; self.window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES; }
The programmer has to start the thread manually and then create the required structure for the thread (entry point, autorelease pool, and thread’s main loop). When we write the same code with GCD, we really won’t have to do much:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); size_t numberOfIterations = 1000; dispatch_async(queue, ^(void) { dispatch_apply(numberOfIterations, queue, ^(size_t iteration){ /* Perform the operation here */ }); });
In this chapter, you will learn all there is to know about GCD and how to use it to write modern multithreaded apps for iOS and Mac OS X that will achieve blazing perform- ance on multicore devices such as the iPad 2.
We will be working with dispatch queues a lot, so please make sure that you fully understand the concept behind them. There are three types of dispatch queues:
Main Queue
This queue performs all its tasks on the main thread, which is where Cocoa and Cocoa Touch require programmers to call all UI-related methods. Use the dispatch_get_main_queue function to retrieve the handle to the main queue.
Concurrent Queues
These are queues that you can retrieve from GCD in order to execute asynchronous or synchronous tasks. Multiple concurrent queues can be executing multiple tasks in parallel, without breaking a sweat. No more thread management, yippee! Use the dispatch_get_global_queue function to retrieve the handle to a concurrent queue.
Serial Queues
These are queues that, no matter whether you submit synchronous or asynchro- nous tasks to them, will always execute their tasks in a first-in-first-out (FIFO) fashion, meaning that they can only execute one block object at a time. However, they do not run on the main thread and therefore are perfect for a series of tasks that have to be executed in strict order without blocking the main thread. Use the dispatch_queue_create function to create a serial queue. Once you are done with the queue, you must release it using the dispatch_release function.
At any moment during the lifetime of your application, you can use multiple dispatch queues at the same time. Your system has only one main queue, but you can create as many serial dispatch queues as you want, within reason, of course, for whatever func- tionality you require for your app. You can also retrieve multiple concurrent queues and dispatch your tasks to them. Tasks can be handed to dispatch queues in two forms: block objects or C functions, as we will see in Recipe 5.4.
Block objects are packages of code that usually appear in the form of methods in Objective-C. Block objects, together with Grand Central Dispatch (GCD), create a harmonious environment in which you can deliver high-performance multithreaded apps in iOS and Mac OS X. What’s so special about block objects and GCD, you might ask? It’s simple: no more threads! All you have to do is to put your code in block objects and ask GCD to take care of the execution of that code for you.
Block objects in Objective-C are what the programming field calls first-class objects. This means you can build code dynamically, pass a block object to a method as a parameter, and return a block object from a method. All of these things make it easier to choose what you want to do at runtime and change the activity of a program. In particular, block objects can be run in individual threads by GCD. Being Objective-C objects, block objects can be treated like any other object.
note: Block objects are sometimes referred to as closures.
Constructing block objects is similar to constructing traditional C functions, as we will see in Recipe 5.1. Block objects can have return values and can accept parameters. Block objects can be defined inline or treated as a separate block of code, similar to a C function. When created inline, the scope of variables accessible to block objects is considerably different from when a block object is implemented as a separate block of code.
GCD works with block objects. When performing tasks with GCD, you can pass a block object whose code can get executed synchronously or asynchronously, depend- ing on which methods you use in GCD. Thus, you can create a block object that is responsible for downloading a URL passed to it as a parameter. That single block object can then be used in various places in your app synchronously or asynchronously, de- pending on how you would like to run it. You don’t have to make the block object synchronous or asynchronous per se; you will simply call it with synchronous or asyn- chronous GCD methods and the block object will just work.
Block objects are quite new to programmers writing iOS and OS X apps. In fact, block objects are not as popular as threads yet, perhaps because their syntax is a bit different from pure Objective-C methods and more complicated. Nonetheless, block objects are enormously powerful and Apple is making a big push toward incorporating them into Apple libraries. You can already see these additions in classes such as NSMutableArray, where programmers can sort the array using a block object.
This chapter is dedicated entirely to constructing and using block objects in iOS and Mac OS X apps, using GCD for dispatching tasks to the operating system, threads and timers. I would like to stress that the only way to get used to block objects’ syntax is to write a few of them for yourself. Have a look at the sample code in this chapter and try implementing your own block objects.
Here, you will learn the basics of block objects, followed by some more advanced sub- jects such as Grand Central Dispatch, Threads, Timers, Operations and Operation Queues. You will understand everything you need to know about block objects before moving to the Grand Central Dispatch material. From my experience, the best way to learn block objects is through examples, so you will see a lot of them in this chapter. Make sure you try the examples for yourself in Xcode to really get the syntax of block objects.
Operations can be configured to run a block of code synchronously or asynchronously. You can manage operations manually or place them on operation queues, which facil- itate concurrency so that you do not need to think about the underlying thread man- agement. In this chapter, you will see how to use operations and operation queues, as well as basic threads and timers, to synchronously and asynchronously execute tasks in applications.
Cocoa provides three different types of operations:
Block operations
These facilitate the execution of one or more block objects.
Invocation operations
These allow you to invoke a method in another, currently existing object.
Plain operations
These are plain operation classes that need to be subclassed. The code to be exe- cuted will be written inside the main method of the operation object.
Operations, as mentioned before, can be managed with operation queues, which have the data type NSOperationQueue. After instantiating any of the aforementioned operation types (block, invocation, or plain operation), you can add them to an operation queue and have the queue manage the operation.
An operation object can have dependencies on other operation objects and be instruc- ted to wait for the completion of one or more operations before executing the task associated with it. Unless you add a dependency, you have no control over the order in which operations run. For instance, adding them to a queue in a certain order does not guarantee that they will execute in that order, despite the use of the term queue.
There are a few important things to bear in mind while working with operation queues and operations:
-
Operations, by default, run on the thread that starts them, using their start in- stance method. If you want the operations to work asynchronously, you will have to use either an operation queue or a subclass NSOperation and detach a new thread on the main instance method of the operation.
-
An operation can wait for the execution of another operation to finish before it starts its execution. Be careful not to create interdependent operations, a common mistake known as a race condition. In other words, do not tell operation A to de- pend on operation B if B already depends on A; this will cause both to wait forever, taking up memory and possibly hanging your application.
-
Operations can be cancelled. So, if you have subclassed NSOperation to create cus- tom operation objects, you have to make sure to use the isCancelled instance method to check whether the operation has been cancelled before executing the task associated with the operation. For instance, if your operation’s task is to check for the availability of an Internet connection every 20 seconds, it must call the isCancelled instance method at the beginning of each run to make sure it has not been cancelled before attempting to check for an Internet connection again. If the operation takes more than a few seconds (such as when you download a file), you should also check isCancelled periodically while running the task.
-
Operation objects are key-value observing (KVO) compliant on various key paths such as isFinished, isReady, and isExecuting. We will be discussing Key Value Coding and Key Value Observing in a later chapter.
-
If you plan to subclass NSOperation and provide a custom implementation for the operation, you must create your own autorelease pool in the main method of the operation, which gets called from the start method. We will discuss this in detail later in this chapter.
-
Always keep a reference to the operation objects you create. The concurrent nature of operation queues might make it impossible for you to retrieve a reference to an operation after it has been added to the queue.
Threads and timers are objects, subclassing NSObject. Threads are more low-level than timers. When an application runs under iOS, the operating system creates at least one thread for that application, called the main thread. Every thread and timer must be added to a run loop. A run loop, as its name implies, is a loop during which different events can occur, such as a timer firing or a thread running. Discussion of run loops is beyond the scope of this chapter, but we will refer to them here and there in recipes.
Think of a run loop as a kind of loop that has a starting point, a condition for finishing, and a series of events to process during its lifetime. A thread or timer is attached to a run loop and in fact requires a run loop to function.
The main thread of an application is the thread that handles the UI events. If you perform a long-running task on the main thread, you will notice that the UI of your application will become unresponsive or slow to respond. To avoid this, you can create separate threads and/or timers, each of which performs its own task (even if it is a long- running task) but will not block the main thread.