Grand Central Dispatch Tutorial for Swift 5: Part 2/2
Learn all about multithreading, dispatch queues, and concurrency in the second part of this Swift 5 tutorial on Grand Central Dispatch. By David Piper.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Grand Central Dispatch Tutorial for Swift 5: Part 2/2
30 mins
Exploring Concurrency Looping
With all these new tools at your disposal, you should probably thread everything, right?!
Take a look at downloadPhotos(withCompletion:)
in PhotoManager
. You might notice that there’s a for
loop in there that cycles through three iterations and downloads three separate images. Your job is to see if you can run this for
loop concurrently to try and speed things up.
This is a job for DispatchQueue.concurrentPerform(iterations:execute:)
. It works like a for
loop in that it executes different iterations concurrently. It’s synchronous and returns only when all work is done.
You must take care when figuring out the optimal number of iterations for a given amount of work. Many iterations and a small amount of work per iteration can create so much overhead that it negates any gains from making the calls concurrent. The technique known as striding helps you out here. Striding allows you to do multiple pieces of work for each iteration.
When is it appropriate to use DispatchQueue.concurrentPerform(iterations:execute:)
? You can rule out serial queues because there’s no benefit there – you may as well use a normal for
loop. It’s a good choice for concurrent queues that contain looping, though, especially if you need to keep track of progress.
In PhotoManager.swift, replace the code inside downloadPhotos(withCompletion:)
with the following:
var storedError: NSError?
let downloadGroup = DispatchGroup()
let addresses = [
PhotoURLString.overlyAttachedGirlfriend,
PhotoURLString.successKid,
PhotoURLString.lotsOfFaces
]
let _ = DispatchQueue.global(qos: .userInitiated)
DispatchQueue.concurrentPerform(iterations: addresses.count) { index in
let address = addresses[index]
guard let url = URL(string: address) else { return }
downloadGroup.enter()
let photo = DownloadPhoto(url: url) { _, error in
storedError = error
downloadGroup.leave()
}
PhotoManager.shared.addPhoto(photo)
}
downloadGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
You replaced the former for
loop with DispatchQueue.concurrentPerform(iterations:execute:)
to handle concurrent looping.
This implementation includes a curious line of code: let _ = DispatchQueue.global(qos: .userInitiated)
. This causes GCD to use a queue with a .userInitiated
quality of service for the concurrent calls.
Build and run the app. Verify that the internet download functionality still behaves properly:
Running this new code on a device will sometimes produce marginally faster results. But was all this work worth it?
Actually, it’s not worth it in this case. Here’s why:
- You’ve probably created more overhead by running the threads in parallel than you would have by just running the
for
loop in the first place. You should useDispatchQueue.concurrentPerform(iterations:execute:)
for iterating over very large sets, along with the appropriate stride length. - You have limited time to create an app — don’t waste time pre-optimizing code that you don’t know is broken. If you’re going to optimize something, do so with something that is noticeable and worth your time. Find the methods with the longest execution times by profiling your app in Instruments. Check out How to Use Instruments in Xcode to learn more.
- Typically, optimizing code makes your code more complicated for yourself and for other developers coming after you. Make sure the added complication is worth the benefit.
Remember, don’t go crazy with optimizations. You’ll only make it harder on yourself and others who have to wade through your code.
Canceling Dispatch Blocks
Thus far, you haven’t seen code that allows you to cancel enqueued tasks. This is where dispatch block objects represented by DispatchWorkItem
comes into focus. Be aware that you can only cancel a DispatchWorkItem
before it reaches the head of a queue and starts executing.
Let’s show this by starting download tasks for several images from Le Internet then canceling some of them.
Still in PhotoManager.swift, replace the code in downloadPhotos(withCompletion:)
with the following:
var storedError: NSError?
let downloadGroup = DispatchGroup()
var addresses = [
PhotoURLString.overlyAttachedGirlfriend,
PhotoURLString.successKid,
PhotoURLString.lotsOfFaces
]
// 1
addresses += addresses + addresses
// 2
var blocks: [DispatchWorkItem] = []
for index in 0..<addresses.count {
downloadGroup.enter()
// 3
let block = DispatchWorkItem(flags: .inheritQoS) {
let address = addresses[index]
guard let url = URL(string: address) else {
downloadGroup.leave()
return
}
let photo = DownloadPhoto(url: url) { _, error in
storedError = error
downloadGroup.leave()
}
PhotoManager.shared.addPhoto(photo)
}
blocks.append(block)
// 4
DispatchQueue.main.async(execute: block)
}
// 5
for block in blocks[3..<blocks.count] {
// 6
let cancel = Bool.random()
if cancel {
// 7
block.cancel()
// 8
downloadGroup.leave()
}
}
downloadGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
Here’s a step-by-step walk-through of the code above:
- You expand the
addresses
array to hold three copies of each image. - You initialize a
blocks
array to hold dispatch block objects for later use. - You create a new
DispatchWorkItem
. You pass in aflags
parameter to specify that the block should inherit its Quality of Service class from the queue you dispatch it to. Then, you define the work to do in a closure. - You dispatch the block asynchronously to the main queue. For this example, using the main queue makes it easier to cancel select blocks since it's a serial queue. The code that sets up the dispatch blocks is already executing on the main queue. Thus, you know that the download blocks will execute at some later time.
- You skip the first three download blocks by slicing the
blocks
array. - Here, you use
Bool.random()
to randomly pick betweentrue
andfalse
. It's like a coin toss. - If the random value is
true
, you cancel the block. This can only cancel blocks that are still in a queue and haven't began executing. You can't cancel a block in the middle of execution. - Here, you remember to remove the canceled block from the dispatch group.
Build and run the app, then add images from Le Internet. You'll see that the app now downloads more than three images. The number of extra images changes each time you re-run your app. You cancel some of the additional image downloads in the queue before they start.
This is a pretty contrived example, but it's a nice illustration of how to use — and cancel — dispatch blocks.
Dispatch blocks can do a lot more, so be sure to check out Apple's documentation.
Miscellaneous GCD Fun
But wait! There’s more! Here are some extra functions that are a little farther off the beaten path. Although you won't use these tools that often, they can be helpful in the right situations.