We all know how to run code after an asynchronous function is finished; simply use the completionHandler
to perform final actions. But what about when we call two asynchronous functions and want to perform a task only after both finish?
In my particular case, I had to make two HTTP requests and update the UI after both had completed.
It would have looked clanky if half the UI updated after a single request finished, and the rest followed after a delay. There were other workarounds, but they were not elegant as they involved storing state. Surely there must be a better way!
Dispatch Groups
Grand Central Dispatch provides a simple solution to this problem. We can use DispatchGroup
to bundle asynchronous tasks together. Once our grouped asynchronous tasks are completed, we will execute a block of code (in this example, we will update our UI).
Defining Two Asynchronous Functions
// Define asynchronous functions
func firstAsyncRequest(completionHandler: @escaping () -> Void) {
// Do Stuff
completionHandler()
}
func secondAsyncRequest(completionHandler: @escaping () -> Void) {
// Do Stuff
completionHandler()
}
Nothing complex here, we just define our asynchronous functions. In a more practical use-case, we would have our closures pass data back to us. But for the sake of simplicity we are skipping that.
Grouping Asynchronous Tasks
let group = DispatchGroup()
// Group our requests:
group.enter()
firstAsyncRequest {
group.leave()
}
group.enter()
secondAsyncRequest {
group.leave()
}
We call group.enter()
before making each request, and group.leave()
in the completionHandler
once the request is finished. We end up with a group that has two tasks added to it, and we specify when each task leaves the group.
The magic happens once all tasks have exited the group.
Performing an Action After Grouped Tasks Finish
group.notify(queue: .main) {
// Update UI
}
DispatchGroup
notifies us once all the tasks we added into the group have left, and performs any block we pass into it. Think of this like as a completion handler, but for a group of tasks 🙂
Precautions
As with anything related to multithreading, there are a few pitfalls we should keep an eye out for.
- Make sure every call to
enter()
has a single corresponding call toleave()
. If you have complex control flow, then callleave()
in numerous places in order to ensure it gets called once. - When our
DispatchGroup
calls notify, be careful to perform the task you pass into it on the correct queue. I needed to update the UI in my example, so I passed in the main queue. - Add all your tasks to the group before calling
notify(queue: execute:)
. That way you ensure it does not trigger before everything has been added to the group and finished executing.
Conclusion
Multithreading can be a pain, but Grand Central Dispatch offers a great deal of tools to simplify the process for developers. One pain-point has been handling multiple asynchronous calls once they are finished. We covered a great solution through the grouping asynchronous calls and utilizing notify(queue: execute:)
. This is a powerful tool found in Grand Central Dispatch, and should be found in every developer’s arsenal. And now it is in yours! 🙂