Using dispatch_async to handle background operations for responsive Cocoa Applications

When your Cocoa application needs to perform slow or indeterminate length operations care must be taken to safeguard the responsiveness of the application. For some longer running operations that are running on the same thread as your application's user interface, this can lead to a Spinning Wait Cursor (aka the "beach ball" or "spinning technicolor pizza of death") while the operation completes. This impacts the user's perception of the responsiveness and quality of the application. It also has the effect of making the user wait when the operation completes before they can interact with the application further.

In this article we will discuss the use of the Grand Central Dispatch function dispatch_async to improve responsiveness and provide appropriate updates to the user interface to provide a fast and responsive user experience.

Grand Central Dispatch

Grand Central Dispatch or GCD is a set of enhancements that help an application to run faster, more efficiently, and with improved reponsiveness. It achieves this be providing functionality that supports concurrent code execution on multicore systems.

We will discuss on the dispatch_async method with a focus on task queue priorities and how you can use them to improve the responsiveness of your Cocoa application. These concepts apply to IOS applications as well.

Handling long-running operations responsively

Let's look an example @IBAction that initiates a long running operation and then updates the user interface based on the results of the operation.

@IBAction func longRunningOperatation(sender : AnyObject) {

    // BAD: a long running operation that returns data that
    // we want to use to update the user interface
    let results = controller.runOperation()

    // a function that uses the results to populates a table
    populateTable(results)
}

When this action is triggered by the user, the main thread of the application will block while waiting for the the return value of the runOperation() function. This will cause the application to become unresponsive, especially if the operation uses a lot of resources with high latency, such as I/O operations or calls to asynchronous operations. The user will also likely see a Spinning Wait Cursor for the duration of this operation.

Consider as an example a file copy operation. If only a few hundred kilobites are being copied, the impact of blocking the main thread will be quite small so as to be (usually) unnoticable. However, if you were copying a large amount of data this would block the application for several seconds or longer, making you application unresponsive for the duration.

Here is a version of the same @IBAction using dispatch_async:

// declare the results variable outside to the scope of the dispatch_async calls
var results : [String]?

@IBAction func longRunningOperatation(sender : AnyObject) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), {        

        // IMPROVED: a long running operation that returns data that
        // we want to use to update the user interface
        self.results = self.controller.runOperation()

        dispatch_async(dispatch_get_main_queue(), {
        // update the UI
        self.populateTable(self.results!)
        });
    });
}

In this version of the @IBAction, we use dispatch_async twice in a block.

Notice that we have to prepend self to functions or variables from the calling class.

The first call to dispatchasync puts our long-running operation in the background by using dispatchgetglobalqueue with an argument of DISPATCHQUEUEPRIORITY_BACKGROUND. There are four possible values for this argument in descending priority:

DISPATCH_QUEUE_PRIORITY_HIGH
DISPATCH_QUEUE_PRIORITY_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOW
DISPATCH_QUEUE_PRIORITY_BACKGROUND

After the results have returned from the long-running operation, we call dispatchasync again, except this time using dispatchgetmainqueue. This function returns the serial queue that is associated with the application's main thread. This is a safe place to make user interface changes. This is especially important when you are making changes that cause the layout engine to re-calculate the layout of the user interface. In fact, you will likely get warnings in Xcode when you try to (for example) start or stop an animation from outside of the application's main thread.

Discussion

When an operation must complete before the user can continue using the application, you can also use dispatchsync which is the synchronous version of dispatchasync. When you do so, you should consider providing some sort of feedback to your user so they know that something important is happening and that the application hasn't frozen. Progress indicators are the obvious choice here.

Conclusion

Using dispatch_async makes it easy for you to create applications that take full advantage of multi-core systems and provides a way to make your application run faster, more responsively and provide a better user experience.

There's a lot more functionality in Grand Central Dispatch than we've covered in this article, you can find out more by reading the Grand Central Dispatch (GCD) reference in full.