Apple's concurrency library is sparsely documented and that discourages developers from using the more advanced APIs of Grand Central Dispatch. This episode focuses on one of those more advanced APIs, dispatch groups.
After reading the documentation of the DispatchGroup class, it isn't immediately obvious how to use dispatch groups and, more importantly, when it's appropriate to use them. The idea is simple, though. A dispatch group enables developers to synchronize work by grouping a set of tasks. Instead of monitoring each task individually, a dispatch group makes it possible to treat the set of tasks as a single task. Don't worry if this doesn't make sense just yet. The example we cover in this episode illustrates what this means.
Dispatch groups aren't used very often and this is in part due to a lack of understanding. Let's start by exploring the inner workings of a dispatch group and when it's appropriate to use one.
What Is It?
Grand Central Dispatch is a concurrency library and it makes it trivial to perform tasks concurrently. Your job as a developer becomes more complex if you need to perform an action when a group of concurrent tasks have completed executing. That is the nature of concurrency.
A dispatch group makes it possible to synchronize work by grouping a set of tasks. It groups a set of tasks and it observes the completion of each task. It doesn't matter which dispatch queue the tasks are submitted to. The API is flexible and thread safe.
The tasks of a dispatch group are usually unrelated. In other words, task A doesn't depend on the result of task B. The power of dispatch groups lies in the concurrent execution of the tasks.
An Example
Before we use dispatch groups in a project, I want to illustrate the API and its possibilities in a playground. Fire up Xcode and create a playground by choosing the Blank template from the iOS > Playground section.

Replace the contents of the playground with an import statement for the Foundation framework.
import Foundation
Create a DispatchGroup instance and store a reference in a constant with name, group.
import Foundation
// Create Dispatch Group
let group = DispatchGroup()
As I mentioned earlier, a dispatch group manages a set of tasks. We notify the dispatch group when we are about to execute a block of work. This is better known as entering the dispatch group. The dispatch group is notified by invoking the enter() method on the DispatchGroup instance.
// Enter Group
group.enter()
As an example, we asynchronously fetch data from a remote server using the URLSession API. We ask the shared URL session for a data task by invoking the dataTask(with:completionHandler:) method. We pass the method a URL instance and a closure. The closure is executed when the request completes, successfully or unsuccessfully. We print a message to indicate that the request completed and, more importantly, we invoke the leave() method on the DispatchGroup instance. By invoking the leave() method, the dispatch group is notified that the block of work has completed executing. This is necessary for the dispatch group to understand when each task has completed executing.
The completion handler of the dataTask(with:completionHandler:) method is executed on the delegate queue of the URLSession instance. This isn't important, though. The DispatchGroup API is thread safe, which means that we don't need to worry about which thread the leave() method is invoked on.
let dataTask0 = URLSession.shared.dataTask(with: URL(string: "https://cocoacasts.com")!) { (data, _, _) in
print("DATA TASK 0 COMPLETED")
// Leave Group
group.leave()
}
We invoke resume() on the URLSessionDataTask instance to initiate the request.
// Resume Data Task
dataTask0.resume()
We repeat this one more time. We invoke the enter() method on the DispatchGroup instance, notifying it that a block of work enters the group. We create another data task to fetch data from a remote server. We print a message in the completion handler and notify the dispatch group that the block of work has completed executing. We invoke resume() on the URLSessionDataTask instance to initiate the request.
// Enter Group
group.enter()
let dataTask1 = URLSession.shared.dataTask(with: URL(string: "https://apple.com")!) { (data, _, _) in
print("DATA TASK 1 COMPLETED")
// Leave Group
group.leave()
}
// Resume Data Task
dataTask1.resume()
We can execute the contents of the playground as is, but that isn't very useful. We're currently not taking advantage of the DispatchGroup class. Remember that the purpose of a dispatch group is to synchronize work. We want to perform an action when the tasks of the dispatch group have completed executing. Grand Central Dispatch offers us two options.
Being Notified
The first option is being notified when the tasks of the dispatch group have completed executing. By invoking the notify method on the DispatchGroup instance, we are notified when the tasks of the dispatch group have completed executing. Grand Central Dispatch defines two variants of the notify method. The API may look familiar.
The first variant accepts a dispatch queue and a closure. It also accepts an optional quality of service class and one or more dispatch work item flags. The second variant accepts a dispatch queue and a dispatch work item. We covered the DispatchWorkItem class earlier in this series.
Let's keep it simple for now and invoke the first variant. We invoke the notify(qos:flags:queue:execute:) method, passing in a reference to the main dispatch queue and a closure. We print the message NOTIFIED in the closure.
// Being Notified
group.notify(queue: .main) {
print("NOTIFIED")
}
Before we execute the contents of the playground, we need to add a few more print statements. The print statements are the key to understand what happens and how a dispatch group works. We print START before creating the DispatchGroup instance.
import Foundation
print("START")
// Create Dispatch Group
let group = DispatchGroup()
We also add a print statement before and after the invocation of the notify(qos:flags:queue:execute:) method.
print("END 1")
// Being Notified
group.notify(queue: .main) {
print("NOTIFIED")
}
print("END 2")
Execute the contents of the playground and inspect the output in Xcode's console. Once you understand how dispatch groups work, the output shouldn't be surprising.
START
END 1
END 2
DATA TASK 0 COMPLETED
DATA TASK 1 COMPLETED
NOTIFIED
The contents of the playground are executed synchronously and that explains the first three lines in the console, START, END 1 and END 2. The data tasks are executed concurrently and asynchronously. The completion handler of each data task is executed when its request completes. The order in which the data tasks complete is unpredictable. Once the data tasks have completed, the closure we passed to the notify(qos:flags:queue:execute:) method is executed and NOTIFIED is printed to the console.
A dispatch group is a simple construct if you break it down to its essentials. Every time the enter() method is invoked, an internal counter is incremented. Every time the leave() method is invoked, the internal counter is decremented. When the internal counter reaches zero, the dispatch group knows that every task has completed executing. It then submits the closure that is passed to the notify(qos:flags:queue:execute:) method to the dispatch queue.
Waiting for Completion
It is at times useful or necessary to block the execution of the current thread until the tasks of the dispatch group have completed executing. The wait() method offers that option. Let's invoke the wait() method on the DispatchGroup instance before the last print statement.
print("END 1")
// Being Notified
group.notify(queue: .main) {
print("NOTIFIED")
}
// Waiting for Completion
group.wait()
print("END 2")
Execute the contents of the playground and inspect the output in the console. Let me explain what happens.
START
END 1
DATA TASK 0 COMPLETED
DATA TASK 1 COMPLETED
END 2
NOTIFIED
The wait() method blocks the execution of the current thread until the tasks of the dispatch group have completed executing. That is why the message of the last print statement, END 2, is preceded by the print statements of the completion handlers of the data tasks.
By blocking the current thread, the work of the dispatch group is synchronized. But it's important to consider the implications. It's fine to use the wait() method on a background thread, but you need to be very, very careful when you use the wait() method on the main thread. Remember that blocking the main thread renders the application unresponsive to the user. I have never seen a valid use case for using the wait() method on the main thread and it isn't something I recommend.
Grand Central Dispatch defines three variants of the wait method. The wait() method we invoked in the playground waits until the tasks of the dispatch group have completed executing. The other variants of the wait method accept a timeout as their only argument. You can pass a DispatchTime instance or a DispatchWallTime instance.
// Waiting for Completion
group.wait()
group.wait(timeout: .now() + 2.5)
group.wait(wallTimeout: .now() + 2.5)
The variants that accept a timeout also return a result. The returned value indicates whether the wait method returned as a result of a timeout. Let's update the current implementation. Store the result of the last wait method in a constant and print its value to the console. Comment out the first and second wait method invocations.
// Waiting for Completion
// group.wait()
// group.wait(timeout: .now() + 2.5)
let result = group.wait(wallTimeout: .now() + 2.5)
print("TIMED OUT \(result == .timedOut)")
Execute the contents of the playground one more time and inspect the output in the console. The result of the wait(wallTimeout:) method depends on the speed of the network connection of your device or your machine.
We covered the difference between DispatchTime and DispatchWallTime earlier in this series. Remember that DispatchTime represents a relative point in time whereas DispatchWallTime represents an absolute point in time.
What's Next?
The playground shows how to use dispatch groups and how they work. But a playground isn't the ideal environment to illustrate why you should be using dispatch groups. Dispatch groups are incredibly useful, but it's important to understand when to use them. In the next episode, I show you an example that highlights the benefits of dispatch groups.