Grand Central Dispatch, or GCD, is an extremely powerful tool. It gives you low level constructs, like queues and semaphores, that you can combine in interesting ways to get useful multithreaded effects. The C-based API used to be a bit arcane, but it’s been cleaned up as of Swift 3. It isn’t always immediately obvious how to combine the low-level components into higher level behaviors, so in this guide, I hope to describe the behaviors that you can create with the low-level components that GCD gives you.
GCD, after many years of loyal service, has started to show its age a little. In pathological cases, it can cause explosions in the number of threads it creates, and can have other, more subtle issues as well. You can read about some of these issues here and here. In this guide, I’ll call out a few things that can help avoid the larger issues, and as always, test the speed of your code before applying concurrency.
Work In The Background
Perhaps the simplest of behaviors, this one lets you do do some work on a background queue, and then come back to the main queue to continue processing, since components like those from UIKit
can (mostly) be used only with the main queue.
(In this guide, I’ll use functions like doSomeExpensiveWork()
to represent some long running task that returns a value. You can imagine this a processing an image, or some other task that takes long enough to slow down the user interface.)
This pattern can be set up like so:
let concurrentQueue = DispatchQueue(label: "com.backgroundqueue", attributes: .concurrent)
// ...
concurrentQueue.async(execute: {
let result = doSomeExpensiveWork()
DispatchQueue.main.async(execute: {
//use `result` somehow
})
})
While DispatchQueue.global()
is a quick way to get access to a concurrent queue, it can easily lead to an explosion of threads. If you create a lot of background tasks all on the .global()
queue, it will create a ton of threads which can counter-intuitively slow your app down, so it is not recommended. In some cases, a serial queue can be effective as well. (If the attributes are not set, it will default the queue to serial.)
Note that each call uses async
, not sync
. async
returns before the block is executed, and sync
waits until the block is finished executing before returning. The inner call can use sync
(because it doesn’t matter when it returns), but the outer call must be async
(otherwise the calling thread, usually the main thread, will be blocked).
Deferring Work
One of the next common usages of Dispatch is performing some work after a specific amount of time. DispatchQueue
has a special method called .asyncAfter
for this.
let delay = DispatchTime.now() + DispatchTimeInterval.seconds(2)
DispatchQueue.main.asyncAfter(deadline: delay, execute: { print("Hello!") })
Work is guaranteed to execute after the deadline, but not necessarily at that exact moment. In other words, it may take longer than the deadline, but it will never happen before the deadline.
Erica Sadun wrote more about the DispatchTime
API here.
Creating singletons
dispatch_once
is an API that can be used to create singletons. It’s no longer necessary in Swift, since there is a simpler way to create singletons. For posterity, however, I’ve included it here (in Objective-C).
+ (instancetype) sharedInstance {
static dispatch_once_t onceToken;
static id sharedInstance;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
To create lazily-constructed, thread-safe singletons in Swift, you can simply use static let
:
class MyType {
static let global = MyType()
}
Flatten a completion block
This is where GCD starts to get interesting. Using a semaphore, we can block a thread for an arbitrary amount of time, until a signal from another thread is sent. Semaphores, like the rest of GCD, are thread-safe, and they can be triggered from anywhere.
Semaphores can be used when there’s an asynchronous API that you need to make synchronous, but you can’t modify it.
// on a background queue
let semaphore = DispatchSemaphore(value: 0)
doSomeExpensiveWorkAsynchronously(completionBlock: {
semaphore.signal()
})
semaphore.wait()
//the expensive asynchronous work is now done
Calling .wait()
will block the thread until .signal()
is called. This means that .signal()
must be called from a different thread, since the current thread is totally blocked. Further, you should never call .wait()
from the main thread, only from background threads.
You can also pass a timeout to wait()
, but I’ve never needed to use one in practice.
It might not be totally obvious why would you want to flatten code that already has a completion block, but it does come in handy. One case where I’ve used it recently is for performing a bunch of asynchronous tasks that must happen serially. A simple abstraction for that use case could be called AsyncSerialWorker
:
typealias DoneBlock = () -> ()
typealias WorkBlock = (DoneBlock) -> ()
class AsyncSerialWorker {
private let serialQueue = DispatchQueue(label: "com.khanlou.serial.queue")
func enqueueWork(work: @escaping WorkBlock) {
serialQueue.async(execute: {
let semaphore = DispatchSemaphore(value: 0)
work({
semaphore.signal()
})
semaphore.wait()
})
}
}
This small class creates a serial queue, and then allows you enqueue work onto the block. The WorkBlock
gives you a DoneBlock
to call when your work is finished, which will trip the semaphore, and allow the serial queue to continue.
Limiting the number of concurrent blocks
In the previous example, the semaphore is used as a simple flag, but it can also be used as a counter for finite resources. If you want to only open a certain number of connections to a specific resource, you can use something like the code below:
class LimitedWorker {
private let serialQueue = DispatchQueue(label: "com.khanlou.serial.queue")
private let concurrentQueue = DispatchQueue(label: "com.khanlou.concurrent.queue", attributes: .concurrent)
private let semaphore: DispatchSemaphore
init(limit: Int) {
semaphore = DispatchSemaphore(value: limit)
}
func enqueue(task: @escaping () -> ()) {
serialQueue.async(execute: {
self.semaphore.wait()
self.concurrentQueue.async(execute: {
task()
self.semaphore.signal()
})
})
}
}
This example is pulled from Apple’s Concurrency Programming Guide. They can explain what’s happening here better than me:
When you create the semaphore, you specify the number of available resources. This value becomes the initial count variable for the semaphore. Each time you wait on the semaphore, the
dispatch_semaphore_wait
function decrements that count variable by 1. If the resulting value is negative, the function tells the kernel to block your thread. On the other end, thedispatch_semaphore_signal
function increments the count variable by 1 to indicate that a resource has been freed up. If there are tasks blocked and waiting for a resource, one of them is subsequently unblocked and allowed to do its work.
The effect is similar to maxConcurrentOperationCount
on NSOperationQueue
. If you’re using raw GCD queues instead of NSOperationQueue
, you can use semaphores to limit the number of blocks that execute simultaneously.
Thanks to Mike Rhodes, this code has been improved from its previous version. He writes:
We use a concurrent queue for executing the user’s tasks, allowing as many concurrently executing tasks as GCD will allow us in that queue. The key piece is a second GCD queue. This second queue is a serial queue and acts as a gatekeeper to the concurrent queue. We wait on the semaphore in the serial queue, which means that we’ll have at most one blocked thread when we reach maximum executing blocks on the concurrent queue. Any other tasks the user enqueues will sit inertly on the serial queue waiting to be executed, and won’t cause new threads to be started.
Wait for many concurrent tasks to finish
If you have many blocks of work to execute, and you need to be notified about their collective completion, you can use a group. DispatchQueue.async
lets you associate a group with the work you’re adding to that queue. The group keeps track of how many items have been associated with it. The work in the block should be synchronous. Note that the same dispatch group can used to track work on multiple different queues. When all of the tracked work is complete, it fires a block passed to .notify()
, kind of like a completion block.
let backgroundQueue = DispatchQueue(label: "com.khanlou.concurrent.queue", attributes: .concurrent)
let group = DispatchGroup()
for item in someArray {
backgroundQueue.async(group: group, execute: {
performExpensiveWork(item: item)
})
}
group.notify(queue: DispatchQueue.main, execute: {
// all the work is complete
})
This is a great case for flattening a function that has a completion block. The dispatch group considers the block to be completed when it returns, so you need the block to wait until the work is complete.
There’s a more manual way to use dispatch groups, especially if your expensive work is already async:
// must be on a background thread
let group = DispatchGroup()
for item in someArray {
group.enter()
performExpensiveAsyncWork(item: item, completionBlock: {
group.leave()
})
}
group.wait()
// all the work is complete
This snippet is more complex, but stepping through it line-by-line can help in understanding it. Like the semaphore, groups also maintain a thread-safe, internal counter that you can manipulate. You can use this counter to make sure multiple long running tasks are all completed before executing a completion block. Using “enter” increments the counter, and using “leave” decrements the counter. Passing it to .async()
on a queue handles all these details for you, so I prefer to use it where possible.
The last thing in this snippet is the wait
call: it blocks the thread and waits for the counter to reach 0 before continuing. Note that you can queue a block with .notify()
even if you use the enter
/leave
APIs. The reverse is also true: you can use the wait
if you use the async
API.
DispatchGroup.wait()
, like DispatchSemaphore.wait()
, accepts a timeout. Again, I’ve never had a need for anything other than the default. Also, just like DispatchSemaphore.wait()
, never call DispatchGroup.wait()
on the main queue.
The biggest difference between the two styles is that the example using notify
can be called entirely from the main queue, whereas the example using wait
must happen on a background queue (at least the wait
part, because it will fully block the current queue).
Isolation Queues
Swift’s Dictionary
(and Array
) types are value types. When they’re modified, their reference is fully replaced with a new copy of the structure. However, because updating instance variables on Swift objects is not atomic, they are not thread-safe. Two threads can update a dictionary (for example by adding a value) at the same time, and both attempt to write at the same block of memory, which can cause memory corruption. We can use isolation queues to achieve thread-safety.
Let’s build an identity map. An identity map is a dictionary that maps items from their ID
property to the model object.
class IdentityMap<T: Identifiable> {
var dictionary = Dictionary<String, T>()
func object(forID id: String) -> T? {
return dictionary[id] as T?
}
func addObject(object: T) {
dictionary[object.id] = object
}
}
This object basically acts as a wrapper around a dictionary. If our function addObject
is called from multiple threads at the same time, it could corrupt the memory, since the threads would be acting on the same reference. This is known as the readers-writers problem. In short, we can have multiple readers reading at the same time, and only one thread can be writing at any given time.
Our ideal case is that reads happen synchronously and concurrently, whereas writes can be asynchronous and must be the only thing happening to the reference. Fortunately, GCD gives us great tools for this exact scenario. GCD’s .barrier
flag does something special when set: it will wait until the queue is totally empty before executing the block. Using the .barrier
flag for our writes will limit access to the dictionary and make sure that we can never have any writes happening at the same time as a read or another write.
class IdentityMap<T: Identifiable> {
var dictionary = Dictionary<String, T>()
private let accessQueue = DispatchQueue(label: "com.khanlou.isolation.queue", attributes: .concurrent)
func object(withID ID: String) -> T? {
return accessQueue.sync(execute: {
return dictionary[ID] as T?
})
}
func addObject(object: T) {
accessQueue.async(flags: .barrier, execute: {
self.dictionary[object.ID] = object
})
}
}
DispatchQueue.sync()
will dispatch the block to our isolation queue and wait for it to be executed before returning. This way, we will have the result of our read synchronously. (If we didn’t make it synchronous, our getter would need a completion block.) Because accessQueue
is concurrent, these synchronous reads will be able to occur simultaneously. Also, note that we’re using a special form of DispatchQueue.sync()
which allows you to return a T
from the inner block and will return that T
from the outer function call. This is new in Swift 3.
accessQueue.async(flags: .barrier, execute: { })
will dispatch the block to the isolation queue. The async
part means it will return before actually executing the block (which performs the write), which means we can continue processing. The .barrier
flag means that it will wait until every currently running block in the queue is finished executing before it executes. Other blocks will queue up behind it and be executed when the barrier dispatch is done.
While this code will definitely work, using async
for writes can be counter-intuitively be less performant. async
calls will create a new thread, if there isn’t one immediately available to run the block. If your write call is faster than 1ms, it might be worth making it .sync
, according to Pierre Habouzit, who worked on this code at Apple. As always, profile before optimizing!
Cancelling blocks
A little known feature of GCD is that blocks can actually be cancelled. Per Matt Rajca, by wrapping a block in a DispatchWorkItem
and using the cancel()
API, you can cancel it.
let work = DispatchWorkItem(block: { print("Hello!") })
let delay = DispatchTime.now() + DispatchTimeInterval.seconds(10)
DispatchQueue.main.asyncAfter(deadline: delay, execute: work)
work.cancel()
After execution of the block starts, it can’t be cancelled. This makes sense, becuase the queue doesn’t have a sense of what’s going on inside your block, or how to cancel it. You can write your own checks into the block, by using work.isCancelled
:
var work: DispatchWorkItem?
work = DispatchWorkItem(block: {
expensiveWorkPart1()
if work?.isCancelled ?? false { return }
expensiveWorkPart2()
})
This is similar to checking isCancelled
within an NSOperation. Note that you have to declare the work
variable as an optional first, even if you don’t initialize the block itself. This is because you will have to use the work
reference inside the block, and Swift won’t let you do it all in one line. This, unlike the other Dispatch APIs, was cleaner in Swift 2, and now requires dealing with optionals to make it work.
Queue Specific Data
The NSThread
object has a threadDictionary
property. You can use this dictionary to store any interesting data. You can do the same with a dispatch queue, using the DispatchQueue.setSpecific
and DispatchQueue.getSpecific
methods. I haven’t thought of any clever ways to use this yet, excepting Benjamin Encz’s method of determining if you’re on the main queue:
struct MainQueueValue { }
DispatchQueue.main.setSpecific(key: DispatchSpecificKey<MainQueueValue>(), value: MainQueueValue())
Now, instead of using [NSThread isMainThread]
, you can instead check DispatchQueue.getSpecific(key: DispatchSpecificKey<MainQueueValue>()) != nil
to determine if you’re on the main queue (as opposed to the main thread, which is subtly different). If you’re on a background queue, you’ll get nil, and if you’re on the main queue, you’ll get an instance of the MainQueueValue
object.
Timer Dispatch Sources
Dispatch sources are a weird thing, and if you’ve made it this far in the handbook, you’ve reached some pretty esoteric stuff. With dispatch sources, you set up a callback up when initializing the dispatch source, and which in triggered when specific events happen. The simplest of these events is a timed event. A simple dispatch timer could be set up like so:
class Timer {
let timer = DispatchSource.makeTimerSource(queue: .main)
init(onFire: @escaping () -> Void, interval: DispatchTimeInterval, leeway: DispatchTimeInterval = .milliseconds(500)) {
timer.schedule(deadline: DispatchTime.now(), repeating: interval, leeway: leeway)
timer.setEventHandler(handler: onFire)
timer.resume()
}
}
Dispatch sources must be explicitly resumed before they will start working.
Custom Dispatch Sources
Another useful type of dispatch source is a custom dispatch source. With a custom dispatch source, you can trigger it any time you want. The dispatch source will coalesce the signals that you send it, and periodically call your event handler. I couldn’t find anything in the documentation defining the policy that guides this coalescing. Here’s an example of an object that adds up data sent in from different threads:
class DataAdder {
let source = DispatchSource.makeUserDataAddSource(queue: .main)
init(onFire: @escaping (UInt) -> Void) {
source.setEventHandler(handler: {
onFire(self.source.data)
})
source.resume()
}
func mergeData(data: UInt) {
source.add(data: data)
}
}
This dispatch source is initialized with a block that will give you the result of all the data that’s been added up so far. You can call addData
from any thread with some amount of data, and the source will manage adding that data up and calling the callback.
You can also use makeUserDataOrSource
instead of makeUserDataAddSource
, which will apply a binary OR to the data:
class DataOrer {
let source = DispatchSource.makeUserDataOrSource()
init(onFire: @escaping (UInt) -> Void) {
source.setEventHandler(handler: {
onFire(self.source.data)
})
source.resume()
}
func mergeData(data: UInt) {
source.or(data: data)
}
}
You could use this to trip a flag from multiple threads. Crucially, the dispatch source’s data is reset to 0 after every each time the block is triggered.
These are the strange depths of GCD. I don’t know how or when I’d use this stuff, but I suspect that when I need it, I’ll be glad that it exists.
Wrap Up
Grand Central Dispatch is a framework with a lot of low-level primitives. Using them, these are the higher-level behaviors I’ve been able to build. If there are any higher-level things you’ve used GCD to build that I’ve left out here, I’d love to hear about them and add them to the list.