- Hands-On Design Patterns with Swift
- Florent Vilmart Giordano Scalzo Sergio De Simone
- 290字
- 2021-07-02 14:45:12
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.