Leave a rating/review
A serial queue runs on a single thread, restricting access to a shared resource to one task at a time. Sometimes, you want to allow more threads, but not an unlimited number of threads. If you’re downloading data from the network, you might want to limit how many downloads happen at once, because you know the data is large and resource-heavy to process.
DispatchSemaphores give you control over how many group tasks run at the same time. It’s a counting semaphore, like a door attendant who only lets you enter a shop when another person has left.
Here’s a simple demonstration of using a DispatchSemaphore. In the starter playground, create a semaphore that allows four concurrent accesses:
let semaphore = DispatchSemaphore(value: 4)
You’re going to simulate performing 10 network downloads, so create a loop that dispatches onto the queue, using the group:
for i in 1...10 {
queue.async(group: group) {
}
}
Next, inside the async closure, ask for permission to use the resource and defer signaling that you’re finished:
semaphore.wait()
defer { semaphore.signal() }
print("Downloading image \(i)")
// Simulate a network wait
Thread.sleep(forTimeInterval: 3)
print("Image \(i) downloaded")
So each download thread waits for the semaphore to give permission and tells the semaphore when it finishes. You tell the thread to sleep for 3 seconds to simulate a network download.
Run the playground up to this line and watch the sidebar and debug console: 4 downloads happen right away; 3 seconds later, another 4 start and finish. Finally, 3 seconds after that, the final 2 complete:
Downloading image 1
Downloading image 2
Downloading image 3
Downloading image 4
Image 4 downloaded
Image 2 downloaded
Image 1 downloaded
Image 3 downloaded
Downloading image 5
Downloading image 6
Downloading image 8
Downloading image 7
Image 6 downloaded
Image 5 downloaded
Image 8 downloaded
Image 7 downloaded
Downloading image 10
Downloading image 9
Image 9 downloaded
Image 10 downloaded
Within each group of 4 images, the download order isn’t fixed.
This was a simple example, just to prove that the semaphore is doing its job of limiting access to the network. Next, you’ll actually download some images! First, comment out the for-loop. Now, here is the code from Images.playground. Add a semaphore argument to dataTask_Group:
func dataTask_Group_Semaphore(with url: URL,
group: DispatchGroup, semaphore: DispatchSemaphore, // add semaphore here
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
As you saw in the simple example, a task must wait for the semaphore before starting, so call wait on the semaphore before entering the group:
semaphore.wait()
group.enter() // existing code
And a task should tell the semaphore when it finishes, so call signal on the semaphore after leaving the group:
defer {
group.leave() // existing code
semaphore.signal()
}
Now, in the for-loop, call dataTask_Group_Semaphore:
dataTask_Group_Semaphore(with: url, group: group, semaphore: semaphore) { data, _, error in
if error == nil, let data, let image = UIImage(data: data) {
images.append(image)
}
}
Run the playground and watch the sidebar and debug console to see how fast the array fills up and note the duration.
Here’s the All done message, and here in the notify closure, click the quick-look icon to see the first image:
Now reduce the semaphore value to 2, then run the playground again.
The tasks now run 2 at a time, so the array fills more slowly and the duration is twice as long.
Reduce it to 1.
Much slower now.
This is the end of part 1: GCD. In Part 2, you’ll learn about concurrency problems and how to manage them.