iOS Components and Frameworks: Understanding the Advanced Features of the iOS SDK (2014)
Chapter 17. Grand Central Dispatch for Performance
Many apps have challenging performance requirements, involving multiple processor-intensive and high-latency tasks that need to take place simultaneously. This chapter demonstrates the negative effects of blocking the main queue, which makes the user interface slow or completely unusable—not at all desirable for a good user experience. It then examines tools supported by iOS that allow the programmer to perform tasks “in the background,” meaning that a processing task will take place and not directly delay updating the user interface. Apple provides several tools with varying degrees of control over how background tasks are accomplished.
Concurrent programming is frequently done using threads. It can be very challenging to get the desired performance improvements from a multicore device by managing threads directly in an app, because effective thread management requires real-time monitoring and management of system resources and usage. To address this problem, Apple introduced Grand Central Dispatch, or GCD. GCD manages queues, which are an abstracted level above threads. Queues can operate concurrently or serially, and can automatically handle thread management and optimization at a system level.
This chapter introduces several approaches to background processing of long-running tasks, and highlights the benefits and drawbacks of each.
The Sample App
The sample app is called LongRunningTasks. It will demonstrate a trivial long-running task on the main thread, and then several different techniques for handling the same long-running task off the main thread. The trivial long-running tasks are five loops to add 10 items each with a time delay to an array, which can then be displayed in a table view. The sample app has a table view, which will present a list of the available approaches. Selecting an approach will present a table view for the approach. The table view has 5 starting items, so it is clear when attempting to scroll whether the main thread is being interrupted by the long-running task. The long-running tasks will then create 50 more items in batches of 10 to display in the table view. The table view will be notified upon completion to update the UI on the main thread, and the new items will appear.
The sample app (as shown in Figure 17.1) will illustrate the following techniques:
performSelectorInBackground:withObject: This is the simplest approach to running code off the main thread and it works well when the task has simple and straightforward requirements. The system does not perform any additional management of tasks performed this way, so this is best suited to nonrepetitive tasks.
NSOperationQueue: This is a slightly more complex method to running code off the main thread, and it provides some additional control capabilities, like running serially, concurrently, or with dependencies between tasks. Operation queues are implemented with and are a higher level of abstraction of GCD queues. Operation queues are best suited to repetitive, well-defined asynchronous tasks; for example, network calls or parsing.
GCD Queues: This is the most “low-level” approach to running code off the main thread, and it provides the most flexibility. The sample app will demonstrate running tasks serially and concurrently using GCD queues. GCD can be used for anything from just communicating between the background and the main queue, to quickly performing a block of code for each item in a list, to processing large, repetitive asynchronous tasks.
Figure 17.1 Sample app, long-running task approach list.
Introduction to Queues
Some of the terminology related to concurrent processing can be a bit confusing. A “thread” is a commonly used term; in the context of an iOS app, a thread is a standard POSIX thread. Technically, a thread is just a set of instructions that can be handled independently within a process (an app is a process), and multiple threads can exist within a process, sharing memory and resources. Since threads function independently, work can be split across threads to get it done more quickly. It is also possible to run into problems when multiple threads need access to the same resource or data. All iOS apps have a main thread that handles the run loop and updating the UI. For an app to remain responsive to user interaction, the main thread must be given only tasks that can be completed in less than 1/60 of a second.
A “queue” is a term that Apple uses to describe the contexts provided by Grand Central Dispatch. A queue is managed by GCD as a group of tasks to be executed. Depending on the current system utilization, GCD will dynamically determine the right number of threads to use to process the tasks in a queue. The main queue is a special queue managed by GCD that is associated with the main thread. So when you run a task on the main queue, GCD executes that task on the main thread.
People will frequently toss around the terms “thread” and “queue” interchangeably; just remember that a queue is really just a managed set of threads, and “main” is just referring to the thread that handles the main run loop and UI.
Running on the Main Thread
Run the sample app and select the row called Main Thread. Notice that the five initial items in the table view are initially visible, but they cannot be scrolled and the UI is completely unresponsive while the additional items are being added. There is logging to demonstrate that the additional items are being added while the UI is frozen—view the debugging console while running the app to see the logging. The frozen UI is obviously not a desirable user experience, and it is unfortunately easy to get a situation like this to happen in an app. To see why this is happening, take a look at the ICFMainThreadLongRunningTaskViewController class. First, the array to store display data is set up and given an initial set of data:
- (void)viewDidLoad
{
[super viewDidLoad];
self.displayItems =
[[NSMutableArray alloc] initWithCapacity:45];
[self.displayItems addObject:@[@"Item Initial-1",
@"Item Initial-2",@"Item Initial-3",
@"Item Initial-4",@"Item Initial-5"]];
}
After the initial data is set up and the view is visible, the long-running task is started:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
for (int i=1; i<=5; i++)
{
NSNumber *iteration = [NSNumber numberWithInt:i];
[self performLongRunningTaskForIteration:iteration];
}
}
The app is calling the performLongRunningTaskForIteration: method five times to set up additional table data. This does not appear to be doing anything that would slow down the main thread. Examine the performLongRunningTaskForIteration: method to see what is hanging the main thread. The intention is for the long-running task to add ten items to an array, which will then be added to the displayItems array, which is the data source for the table view.
- (void)performLongRunningTaskForIteration:(id)iteration
{
NSNumber *iterationNumber = (NSNumber *)iteration;
NSMutableArray *newArray =
[[NSMutableArray alloc] initWithCapacity:10];
for (int i=1; i<=10; i++)
{
[newArray addObject:
[NSString stringWithFormat:@"Item %@-%d",
iterationNumber,i]];
[NSThread sleepForTimeInterval:.1];
NSLog(@"Main Added %@-%d",iterationNumber,i);
}
[self.displayItems addObject:newArray];
[self.tableView reloadData];
}
Since the main thread is responsible for keeping the user interface updated, any activity that takes longer than 1/60 of a second can result in noticeable delays. In this case, notice that the method is calling sleepForTimeInterval: on NSThread for every iteration. Obviously, this is not something that would make sense to do in a typical app, but it clearly illustrates the point that a single method call that takes a little bit of time and blocks the main thread can cause severe performance issues.
Note
Finding the method calls that take up an appreciable amount of time is rarely as clear as this example. Refer to Chapter 25, “Debugging and Instruments,” for techniques to discover where performance issues are taking place.
In this case, the sleepForTimeInterval: method call is quickly and frequently blocking the main thread, so even after the for loop is complete, the main thread does not have enough time to update the UI until all the calls to performLongRunningTaskForIteration: are complete.
Running in the Background
Run the sample app, and select the row called Perform Background. Notice that the five initial items in the table view are initially visible, and they are scrollable while the long-running tasks are being processed (view the debugging console to confirm that they are being processed while scrolling the table view). After the tasks are completed, the additional rows become visible.
This approach sets up the initial data in exactly the same way as the Main Thread approach. View ICFPerformBackgroundViewController in the sample app source code to see how it is set up. After the initial data is set up and the view is visible, the long-running task is started; this is where performing the task in the background is specified.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
SEL taskSelector =
@selector(performLongRunningTaskForIteration:);
for (int i=1; i<=5; i++)
{
NSNumber *iteration = [NSNumber numberWithInt:i];
[self performSelectorIn Background:taskSelector
withObject:iteration];
}
}
A selector is set up; this is just the name of the method to perform in the background. NSObject defines the method performSelectorInBackground:withObject:, which requires an Objective-C object to be passed as the parameter withObject:. This method will spawn anew thread, execute the method with the passed parameter in that new thread, and return to the calling thread immediately. This new thread is the developer’s responsibility to manage, so it is entirely possible to create too many new threads and overwhelm the system. If testing indicates that this is a problem, an operation queue or dispatch queue (both described later in the chapter) can be used to provide more precise control over the execution of the tasks and better management of system resources.
The method performLongRunningTaskForIteration: performs exactly the same task as in the Main Thread approach; however, instead of adding the newArray to the displayItems array directly, the method calls the updateTableData: method using NSObject’s method performSelectorOnMainThread:withObject:waitUntilDone:. Using that approach is necessary for two reasons. First, UIKit objects, including our table view, will update the UI only if they are updated on the main thread. Second, the property displayItems is declared as nonatomic, meaning that the getter and setter methods generated are not thread-safe. That could be “fixed” by declaring the displayItems property atomic, but that would add some performance overhead to lock the array before updating it. If the property is updated on the main thread, locking is not required.
- (void)performLongRunningTaskForIteration:(id)iteration
{
NSNumber *iterationNumber = (NSNumber *)iteration;
NSMutableArray *newArray =
[[NSMutableArray alloc] initWithCapacity:10];
for (int i=1; i<=10; i++)
{
[newArray addObject:
[NSString stringWithFormat:@"Item %@-%d",
iterationNumber,i]];
[NSThread sleepForTimeInterval:.1];
NSLog(@"Background Added %@-%d",iterationNumber,i);
}
[self performSelectorOnMainThread:@selector(updateTableData:)
withObject:newArray
waitUntilDone:NO];
}
The updateTableData: method simply adds the newly created items to the displayItems array and informs the table view to reload and update the UI.
An interesting side effect is that the order in which the additional rows are added is not deterministic—it will potentially be different every time the app is run.
10:51:09.324 LongRunningTasks[29382:15903] Background Added 3-1
10:51:09.324 LongRunningTasks[29382:16303] Background Added 5-1
10:51:09.324 LongRunningTasks[29382:15207] Background Added 1-1
10:51:09.324 LongRunningTasks[29382:15e03] Background Added 4-1
10:51:09.324 LongRunningTasks[29382:15107] Background Added 2-1
10:51:09.430 LongRunningTasks[29382:15207] Background Added 1-2
10:51:09.430 LongRunningTasks[29382:16303] Background Added 5-2
10:51:09.430 LongRunningTasks[29382:15e03] Background Added 4-2
10:51:09.430 LongRunningTasks[29382:15107] Background Added 2-2
10:51:09.430 LongRunningTasks[29382:15903] Background Added 3-2
...
This is a symptom of the fact that using this technique makes no promises about when a task will be completed or in what order it will be processed, since the tasks are all performed on different threads. If the order of operation is not important, this technique can be just fine; if order matters, an operation queue or dispatch queue is needed to process the tasks serially (both described later, in the sections “Serial Operations” and “Serial Dispatch Queues”).
Running in an Operation Queue
Operation queues (NSOperationQueue) are available to manage a set of tasks or operations (NSOperation). An operation queue can specify how many operations can run concurrently, can be suspended and restarted, and can cancel all pending operations. Operations can be a simple method invocation, a block, or a custom operation class. Operations can have dependencies established to make them run serially. Operations and operation queues are actually managed by Grand Central Dispatch, and are implemented with dispatch queues.
The sample app illustrates three approaches using an operation queue: concurrent operations, serial operations with dependencies, and custom operations to support cancellation.
Concurrent Operations
Run the sample app, and select the row called Operation Queue-Concurrent. The five initial items in the table view are visible, and they are scrollable while the long-running tasks are being processed (view the debugging console to confirm that they are being processed while scrolling the table view). After the tasks are completed, the additional rows become visible.
Examine the ICFOperationQueueConcurrentViewController in the sample app source code to see how this approach is set up. Before operations are added, the operation queue needs to be set up in the viewDidLoad: method:
self.processingQueue = [[NSOperationQueue alloc] init];
This approach sets up the initial data in exactly the same way as the Main Thread approach. After the initial data is set up and the view is visible, the long-running tasks are added to the operation queue as instances of NSInvocationOperation:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
SEL taskSelector =
@selector(performLongRunningTaskForIteration:);
for (int i=1; i<=5; i++)
{
NSNumber *iteration = [NSNumber numberWithInt:i];
NSInvocationOperation *operation =
[[NSInvocationOperation alloc] initWithTarget:self
selector:taskSelector object:iteration];
[operation setCompletionBlock:^{
NSLog(@"Operation #%d completed.",i);
}];
[self.processingQueue addOperation:operation];
}
}
Each operation is assigned a completion block that will run when the operation is finished processing. The method performLongRunningTaskForIteration: performs exactly the same task as in the Perform Background approach; in fact, the method is not changed in this approach. The updateTableData: method is also not changed. The results will be very similar to the Perform Background approach in that the items will not be added in a deterministic order.
21:00:16.165 LongRunningTasks[31009] OpQ Concurrent Added 1-1
21:00:16.165 LongRunningTasks[31009] OpQ Concurrent Added 3-1
21:00:16.165 LongRunningTasks[31009] OpQ Concurrent Added 4-1
21:00:16.165 LongRunningTasks[31009] OpQ Concurrent Added 2-1
21:00:16.165 LongRunningTasks[31009] OpQ Concurrent Added 5-1
...
21:00:17.107 LongRunningTasks[31009] Operation #4 completed.
21:00:17.108 LongRunningTasks[31009] Operation #2 completed.
21:00:17.107 LongRunningTasks[31009] Operation #5 completed.
21:00:17.108 LongRunningTasks[31009] Operation #3 completed.
21:00:17.109 LongRunningTasks[31009] Operation #1 completed.
The main difference here is that the NSOperationQueue is managing the threads, and will process only up to the default maximum concurrent operations for the queue. This can be very important when your app has many different competing tasks that need to happen concurrently and need to be managed to avoid overloading the system.
Note
The default maximum concurrent operations for an operation queue is a dynamic number determined in real time by the system. It can vary based on the current system load. The maximum number of operations can also be specified for an operation queue, in which case the queue will process only up to the specified number of operations simultaneously.
Serial Operations
Visit the sample app, and select the row called Operation Queue-Serial. The five initial items in the table view are visible, and they are scrollable while the long-running tasks are being processed (view the debugging console to confirm that they are being processed while scrolling the table view). After the tasks are completed, the additional rows become visible.
The setups of the initial data and operation queue are identical to the Operation Queue-Concurrent approach. To have the operations process serially in the correct order, they need to be set up with dependencies. To accomplish this task, the viewDidAppear: method adds an array to store the operations as they are created, and an NSInvocationOperation (prevOperation) to track the previous operation created.
NSMutableArray *operationsToAdd = [[NSMutableArray alloc] init];
NSInvocationOperation *prevOperation = nil;
While the operations are being created, the method keeps track of the previously created operation. The newly created operation adds a dependency to the previous operation so that it cannot run until the previous operation completes. The new operation is added to the array of operations to add to the queue.
for (int i=1; i<=5; i++)
{
NSNumber *iteration = [NSNumber numberWithInt:i];
NSInvocationOperation *operation =
[[NSInvocationOperation alloc] initWithTarget:self
selector:taskSelector object:iteration];
if (prevOperation)
{
[operation addDependency:prevOperation];
}
[operationsToAdd addObject:operation];
prevOperation = operation;
}
After all the operations are created and added to the array, they are added to the queue. Because an operation will start executing as soon as it is added to an operation queue, the operations need to be added all at once to ensure that the queue can respect the dependencies.
for (NSInvocationOperation *operation in operationsToAdd)
{
[self.processingQueue addOperation:operation];
}
The operation queue will analyze the added operations and dependencies, and determine the optimum order in which to execute them. Observe the debugging console to see that the operations execute in the correct order serially.
16:51:45.216 LongRunningTasks[29554:15507] OpQ Serial Added 1-1
16:51:45.318 LongRunningTasks[29554:15507] OpQ Serial Added 1-2
16:51:45.420 LongRunningTasks[29554:15507] OpQ Serial Added 1-3
16:51:45.522 LongRunningTasks[29554:15507] OpQ Serial Added 1-4
16:51:45.625 LongRunningTasks[29554:15507] OpQ Serial Added 1-5
16:51:45.728 LongRunningTasks[29554:15507] OpQ Serial Added 1-6
16:51:45.830 LongRunningTasks[29554:15507] OpQ Serial Added 1-7
16:51:45.931 LongRunningTasks[29554:15507] OpQ Serial Added 1-8
16:51:46.034 LongRunningTasks[29554:15507] OpQ Serial Added 1-9
16:51:46.137 LongRunningTasks[29554:15507] OpQ Serial Added 1-10
16:51:46.246 LongRunningTasks[29554:14e0b] OpQ Serial Added 2-1
16:51:46.349 LongRunningTasks[29554:14e0b] OpQ Serial Added 2-2
16:51:46.452 LongRunningTasks[29554:14e0b] OpQ Serial Added 2-3
16:51:46.554 LongRunningTasks[29554:14e0b] OpQ Serial Added 2-4
16:51:46.657 LongRunningTasks[29554:14e0b] OpQ Serial Added 2-5
16:51:46.765 LongRunningTasks[29554:14e0b] OpQ Serial Added 2-6
...
The serial approach increases the total amount of time needed to complete all the tasks, but successfully ensures that the tasks execute in the correct order.
Canceling Operations
Back in the sample app, select the row called Operation Queue-Concurrent. Quickly tap the Cancel button at the top of the table while the operations are running. Notice that nothing appears to happen after the Cancel button has been tapped, and the operations will finish. When the Cancel button is touched, the operation queue is instructed to cancel all outstanding operations:
- (IBAction)cancelButtonTouched:(id)sender
{
[self.processingQueue cancelAllOperations];
}
The reason this does not work as expected is because Cancelled is just a flag on an operation object—the logic of the operation must dictate how the operation behaves when it is canceled. The call to cancelAllOperations: dutifully sets the flag on all the outstanding operations, and since they are not checking their own cancellation status while running, they proceed until they complete their tasks.
To properly handle cancellation, a subclass of NSOperation must be created, or a weak reference to an instance of NSBlockOperation must be checked like so:
NSBlockOperation *blockOperation =
[[NSBlockOperation alloc] init];
__weak NSBlockOperation *blockOperationRef = blockOperation;
[blockOperation addExecutionBlock:^{
if (![blockOperationRef isCancelled])
{
NSLog(@"...not canceled, execute logic here");
}
}];
In the next section, creating a custom NSOperation subclass with cancellation handling is discussed.
Custom Operations
Return to the sample app, and select the row called Operation Queue-Custom. The five initial items in the table view are visible, and they are scrollable while the long-running tasks are being processed (view the debuggving console to confirm that they are being processed while scrolling the table view). Quickly hit the Cancel button at the top of the table before the operations complete. Notice that this time the tasks stop immediately.
The setups of the initial data and operation queue are nearly identical to the Operation Queue-Serial approach. The only difference is the use of a custom NSOperation subclass called ICFCustomOperation:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSMutableArray *operationsToAdd =
[[NSMutableArray alloc] init];
ICFCustomOperation *prevOperation = nil;
for (int i=1; i<=5; i++)
{
NSNumber *iteration = [NSNumber numberWithInt:i];
ICFCustomOperation *operation =
[[ICFCustomOperation alloc] initWithIteration:iteration
andDelegate:self];
if (prevOperation)
{
[operation addDependency:prevOperation];
}
[operationsToAdd addObject:operation];
prevOperation = operation;
}
for (ICFCustomOperation *operation in operationsToAdd)
{
[self.processingQueue addOperation:operation];
}
}
ICFCustomOperation is declared as a subclass of NSOperation, and declares a protocol so that it can inform a delegate that processing is complete and pass back the results.
@protocol ICFCustomOperationDelegate <NSObject>
- (void)updateTableWithData:(NSArray *)moreData;
@end
An NSOperation subclass needs to implement the main method. This is where the processing logic for the operation should go:
- (void)main
{
NSMutableArray *newArray =
[[NSMutableArray alloc] initWithCapacity:10];
for (int i=1; i<=10; i++)
{
if ([self isCancelled])
{
break;
}
[newArray addObject:
[NSString stringWithFormat:@"Item %@-%d",
self.iteration,i]];
[NSThread sleepForTimeInterval:.1];
NSLog(@"OpQ Custom Added %@-%d",self.iteration,i);
}
[self.delegate updateTableWithData:newArray];
}
At the beginning of the for loop, the cancellation status is checked:
if ([self isCancelled])
{
break;
}
This check allows the operation to respond immediately to a cancellation request. When designing a custom operation, give careful consideration to how cancellations should be processed, and whether any rollback logic is required.
Properly handling cancellations is not the only benefit to creating a custom operation subclass; it is a very effective way to encapsulate complex logic in a way that can be run in an operation queue.
Running in a Dispatch Queue
Dispatch queues are provided by Grand Central Dispatch to execute blocks of code in a managed environment. GCD is designed to maximize concurrency and take full advantage of multicore processing power by managing the number of threads allocated to a queue dynamically based on the status of the system.
GCD offers three types of queues: the main queue, concurrent queues, and serial queues. The main queue is a special queue created by the system, which is tied to the application’s main thread. In iOS, three concurrent queues are available: the high-, normal-, and low-priority queues. Serial queues can be created by the application and must be managed like any other application resource. The sample app demonstrates using concurrent and serial GCD queues, and how to interact with the main queue from those queues.
Note
As of iOS 6, created dispatch queues are managed by ARC, and do not need to be retained or released.
Concurrent Dispatch Queues
Open the sample app, and select the row called Dispatch Queue-Concurrent. The five initial items in the table view are visible, and they are scrollable while the long-running tasks are being processed (view the debugging console to confirm that they are being processed while scrolling the table view). After the tasks are completed, the additional rows become visible. Notice that this approach completes significantly faster than any of the previous approaches.
To start the long-running tasks in the viewDidAppear: method, the app gets a reference to the high-priority concurrent dispatch queue:
dispatch_queue_t workQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
Using dispatch_get_global_queue provides access to the three global, system-maintained concurrent dispatch queues. References to these queues do not need to be retained or released. After a reference to the queue is available, logic can be submitted to it for execution inside a block.
for (int i=1; i<=5; i++)
{
NSNumber *iteration = [NSNumber numberWithInt:i];
dispatch_async(workQueue, ^{
[self performLongRunningTaskForIteration:iteration];
});
}
The use of dispatch_async indicates that the logic should be performed asynchronously. In that case, the work in the block will be submitted to the queue, and the call to do that will return immediately without blocking the main thread. Blocks can also be submitted to the queue synchronously using dispatch_sync, which will cause the calling thread to wait until the block has completed processing.
In the performLongRunningTaskForIteration: method, there are a few differences from previous approaches that need to be highlighted. The newArray used to keep track of created items needs a __block modifier so that the block can update it.
__block NSMutableArray *newArray =
[[NSMutableArray alloc] initWithCapacity:10];
The method then gets a reference to the low-priority concurrent dispatch queue.
dispatch_queue_t detailQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
The low-priority dispatch queue is then used for a powerful GCD technique: processing an entire enumeration simultaneously.
dispatch_apply(10, detailQueue, ^(size_t i)
{
[NSThread sleepForTimeInterval:.1];
[newArray addObject:[NSString stringWithFormat:
@"Item %@-%zu",iterationNumber,i+1]];
NSLog(@"Dispatch Q Added %@-%zu",iterationNumber,i+1);
});
With dispatch_apply, all that is needed is a number of iterations, a reference to a dispatch queue, a variable to express which iteration is being processed (it must be size_t), and a block to be processed for each iteration. GCD will fill the queue with all the possible iterations, and they will be processed as close to simultaneously as possible, within the constraints of the system. This is the technique that allows this approach to process so much more quickly than the other approaches, and it can be very effective if order is not important.
Note
There are methods on collection classes that can achieve a similar effect at a higher level of abstraction. For example, NSArray has a method called enumerateObjectsWithOptions:usingBlock:. This method can enumerate the objects in an array serially, serially in reverse, or concurrently.
After the iterations are complete and the array of new items has been created, the method needs to inform the UI to update the table view.
dispatch_async(dispatch_get_main_queue(), ^{
[self updateTableData:newArray];
});
This dispatch_async call uses the function dispatch_get_main_queue to access the main queue. Note that this technique can be used from anywhere to get to the main queue, and can be a very handy way to update the UI to report status on long-running tasks.
Serial Dispatch Queues
Run the sample app, and select the row called Dispatch Queue-Serial. The five initial items in the table view are visible, and they are scrollable while the long-running tasks are being processed (view the debugging console to confirm that they are being processed while scrolling the table view). After the tasks are completed, the additional rows become visible. This approach is not as fast as the Concurrent Dispatch Queue approach, but it will process the items in the order in which they are added to the queue.
To start the long-running tasks in the viewDidAppear: method, the app creates a serial dispatch queue:
dispatch_queue_t workQueue =
dispatch_queue_create("com.icf.serialqueue", NULL);
Blocks of work can be added to the serial queue asynchronously:
for (int i=1; i<=5; i++)
{
NSNumber *iteration = [NSNumber numberWithInt:i];
dispatch_async(workQueue, ^{
[self performLongRunningTaskForIteration:iteration];
});
}
The method performLongRunningTaskForIteration: performs exactly the same task as in the Main Thread, Perform Background, and Concurrent Operation Queue approach; however, the method calls the updateTableData: method using dispatch_async to the main queue.
- (void)performLongRunningTaskForIteration:(id)iteration
{
NSNumber *iterationNumber = (NSNumber *)iteration;
NSMutableArray *newArray =
[[NSMutableArray alloc] initWithCapacity:10];
for (int i=1; i<=10; i++)
{
[newArray addObject:[NSString stringWithFormat:
@"Item %@-%d",iterationNumber,i]];
[NSThread sleepForTimeInterval:.1];
NSLog(@"DispQ Serial Added %@-%d",iterationNumber,i);
}
dispatch_async(dispatch_get_main_queue(), ^{
[self updateTableData:newArray];
});
}
The serial dispatch queue will execute the long-running tasks in the order in which they are added to the queue, first in, first out. The debugging console will show that the operations execute in the correct order.
20:41:00.340 LongRunningTasks[30650:] DispQ Serial Added 1-1
20:41:00.444 LongRunningTasks[30650:] DispQ Serial Added 1-2
20:41:00.546 LongRunningTasks[30650:] DispQ Serial Added 1-3
20:41:00.648 LongRunningTasks[30650:] DispQ Serial Added 1-4
20:41:00.751 LongRunningTasks[30650:] DispQ Serial Added 1-5
20:41:00.852 LongRunningTasks[30650:] DispQ Serial Added 1-6
20:41:00.955 LongRunningTasks[30650:] DispQ Serial Added 1-7
20:41:01.056 LongRunningTasks[30650:] DispQ Serial Added 1-8
20:41:01.158 LongRunningTasks[30650:] DispQ Serial Added 1-9
20:41:01.261 LongRunningTasks[30650:] DispQ Serial Added 1-10
20:41:01.363 LongRunningTasks[30650:] DispQ Serial Added 2-1
20:41:01.465 LongRunningTasks[30650:] DispQ Serial Added 2-2
20:41:01.568 LongRunningTasks[30650:] DispQ Serial Added 2-3
20:41:01.671 LongRunningTasks[30650:] DispQ Serial Added 2-4
20:41:01.772 LongRunningTasks[30650:] DispQ Serial Added 2-5
...
Again, the serial approach increases the total amount of time needed to complete all the tasks, but successfully ensures that the tasks execute in the correct order. Using a dispatch queue to process tasks serially is simpler than managing dependencies in an operation queue, but does not offer the same high-level management options.
Summary
This chapter introduced several techniques to process long-running tasks without interfering with the UI, including performSelectorInBackground:withObject:, operation queues, and Grand Central Dispatch queues.
Using the performSelectorInBackground:withObject: method on NSObject to execute a task in the background is the simplest approach, but provides the least support and management.
Operation queues can process tasks concurrently or serially, using a method call, a block, or a custom operation class. Operation queues can be managed by specifying a maximum number of concurrent operations, they can be suspended and resumed, and all the outstanding operations can be canceled. Operation queues can handle custom operation classes. Operation queues are implemented by Grand Central Dispatch.
Dispatch queues can also process tasks concurrently or serially. There are three global concurrent queues, and applications can create their own serial queues. Dispatch queues accept blocks to be executed, and can execute blocks synchronously or asynchronously.
None of these techniques is described as “the best,” because each technique has pros and cons relative to the requirements of an application, and testing should be done to understand which technique is most appropriate.
Exercises
1. Compare the thread handling of the Perform Background, Operation Queue-Concurrent, and Dispatch Queue-Concurrent approaches by cranking up the number of tasks to be performed from 5 to 100, and determine how that affects UI responsiveness and the total time to complete all the tasks by approach.
2. Replace the trivial long-running task with a real task, like parsing lots of JSON files or some image processing. Determine which approach is most effective for the type of task selected.