Using groups

Dispatch groups let you organize tasks and execute a block when many tasks have completed. Let's suppose that we need to wait for many long-running tasks to complete, but we don't really know ahead of time how many tasks we need to run, so using a semaphore is not a good solution. For this particular problem, using DispatchGroup is one of the best-suited solutions:

/**
Performs some work on any thread
- parameter done: A block that will be called when the work is done
- note: This method may not be thread safe
*/
func doWork(done: @escaping () -> ()) {
/* complex implementation doing important things */
}

Consider the previous function, which could be provided by a third-party SDK or your own API. It performs some work, and, at a later point, the callback, done will be called.

Now, let's suppose that we need to execute it a certain number of times; we could write it as follows:

// completion block
let complete = {
print("DONE!")
}
// initialize a count for the remaining operations
var count = 0
for i in 0..<4 {
count += 1 // add one operation
doWork {
count -= 1 // one operation is done
if count == 0 { // yay! we're finished
complete()
}
}
}

This is very, very bad code, for many reasons, including the following:

  • Poor readability
  • Unsafe access to the count variable from many threads
  • Not reusable

For all of these issues, use DispatchGroup, as follows:

let group = DispatchGroup()

// Iterate through all our tasks
for i in 1..<4 {
// tell the group we're adding additional work
group.enter()
// Do the piece of work
doWork {
// tell the group the work is done
group.leave()
}
}

// tell the group to call complete when done
group.notify(queue: .main, execute: complete)

As you can see, this approach has many benefits:

  • The code is more readable and easier to follow
  • There is no unsafe incrementation of variables
  • You have better control over the execution of your completion block
  • It allows for higher order abstractions

Let's take a look at an abstraction over DispatchGroup that you can use in your projects to synchronize many executions together:

// Typealiases so it's easier to reference them all
typealias
Block = () -> ()
typealias FunctionWithCallback = (@escaping Block) -> ()

/**
Runs asynchronous functions and calls completion when all is done
- parameter functions: List of functions to run
- parameter completion: A block to call when all functions have completed
*/
func runAll(functions: [FunctionWithCallback], completion: @escaping Block) {
// Create a group
let group = DispatchGroup()
functions.forEach { (function) in
group.enter()
function {
group.leave()
}
}
group.notify(queue: .main, execute: completion)
}

In the preceding example, we created a very high abstraction over simple invocations that complete in the future; thanks to DispatchGroup, this implementation is thread safe, easy to understand and maintain, and highly reusable.