Chapters

Hide chapters

Concurrency by Tutorials

Second Edition · iOS 13 · Swift 5.1 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

4. Groups & Semaphores
Written by Scott Grosch

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Sometimes, instead of just tossing a job into a queue, you need to process a group of jobs. They don’t all have to run at the same time, but you need to know when they have all completed. Apple provides Dispatch Groups for this exact scenario.

DispatchGroup

The aptly named DispatchGroup class is what you’ll use when you want to track the completion of a group of tasks.

You start by initializing a DispatchGroup. Once you have one and want to track a task as part of that group, you can provide the group as an argument to the async method on any dispatch queue:

let group = DispatchGroup()

someQueue.async(group: group) { ... your work ... } 
someQueue.async(group: group) { ... more work .... }
someOtherQueue.async(group: group) { ... other work ... } 

group.notify(queue: DispatchQueue.main) { [weak self] in
  self?.textLabel.text = "All jobs have completed"
}

As seen in the example code above, groups are not hardwired to a single dispatch queue. You can use a single group, yet submit jobs to multiple queues, depending on the priority of the task that needs to be run. DispatchGroups provide a notify(queue:) method, which you can use to be notified as soon as every job submitted has finished.

Note: The notification is itself asynchronous, so it’s possible to submit more jobs to the group after calling notify, as long as the previously submitted jobs have not already completed.

You’ll notice that the notify method takes a dispatch queue as a parameter. When the jobs are all finished, the closure that you provide will be executed in the indicated dispatch queue. The notify call shown is likely to be the version you’ll use most often, but there are a couple other versions which allow you to specify a quality of service as well, for example.

Synchronous waiting

There be dragons here!

let group = DispatchGroup()

someQueue.async(group: group) { ... }
someQueue.async(group: group) { ... }
someOtherQueue.async(group: group) { ... } 

if group.wait(timeout: .now() + 60) == .timedOut {
  print("The jobs didn’t finish in 60 seconds")
}
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInitiated)

queue.async(group: group) {
  print("Start job 1")
  Thread.sleep(until: Date().addingTimeInterval(10))
  print("End job 1")
 }

queue.async(group: group) {
  print("Start job 2")
  Thread.sleep(until: Date().addingTimeInterval(2))
  print("End job 2")
 }
if group.wait(timeout: .now() + 5) == .timedOut {
  print("I got tired of waiting")
} else {
  print("All the jobs have completed")
}

Wrapping asynchronous methods

A dispatch queue natively knows how to work with dispatch groups, and it takes care of signaling to the system that a job has completed for you. In this case, completed means that the code block has run its course. Why does that matter? Because if you call an asynchronous method inside of your closure, then the closure will complete before the internal asynchronous method has completed.

queue.dispatch(group: group) {
  // count is 1
  group.enter()
  // count is 2
  someAsyncMethod { 
    defer { group.leave() }
    
    // Perform your work here,
    // count goes back to 1 once complete
  }
}
func myAsyncAdd(
  lhs: Int, 
  rhs: Int, 
  completion: @escaping (Int) -> Void) {
  // Lots of cool code here
  completion(lhs + rhs)
}

func myAsyncAddForGroups(
  group: DispatchGroup, 
  lhs: Int, 
  rhs: Int, 
  completion: @escaping (Int) -> Void) {
    group.enter()

    myAsyncAdd(first: first, second: second) { result in
      defer { group.leave() }
      completion(result)
    }
}

Downloading images

Performing a network download should always be an asynchronous operation. This book’s technical editor once had an assignment that required him to download all of the player’s avatars before presenting the user with a list of players and their images. A dispatch group is a perfect solution for that task.

for id in ids {
  guard let url = URL(string: "\(base)\(id)-jpeg.jpg") else { continue }
group.enter()

let task = URLSession.shared.dataTask(with: url) { 
  data, _, error in
defer { group.leave() }
    if error == nil, let data = data, let image = UIImage(data: data) {
      images.append(image)
    }
  }

  task.resume()
}
group.notify(queue: queue) {
  images[0]
  
  PlaygroundPage.current.finishExecution()
}

Semaphores

There are times when you really need to control how many threads have access to a shared resource. You’ve already seen the read/write pattern to limit access to a single thread, but there are times when you can allow more resources to be used at once while still maintaining control over the total thread count.

let semaphore = DispatchSemaphore(value: 4)
for i in 1...10 {
  queue.async(group: group) {

  }
}
semaphore.wait()
defer { semaphore.signal() }

print("Downloading image \(i)")

// Simulate a network wait
Thread.sleep(forTimeInterval: 3)

print("Downloaded image \(i)")
semaphore.wait()
defer {
  group.leave()
  semaphore.signal()
}
let hammer = DispatchSemaphore(value: 3)
let saw = DispatchSemaphore(value: 4)

Where to go from here?

Modify the various values in the playgrounds to be sure you understand how both groups and semaphores work.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now