My notes on Parallel Execution
concurrentPerform
Grand Central Dispatch
implements an efficient parallel for-loop. It must be called on a specific queue not to accidentally block the main one:
DispatchQueue.global().async {
DispatchQueue.concurrentPerform(iterations: 999) { index in
// Do something
}
}
Do many things simultaneously and call onComplete when things are done
/**
* - Abstract: process data in parallel on a background thread and calls a onComplete when it's complete
* ## Examples:
* processData { Swift.print("✅") } // Output: start, 1, 2, 0, 3, ✅
*/
func processData(onComplete: @escaping () -> Void) {
Swift.print("start")
DispatchQueue.global().async {
DispatchQueue.concurrentPerform(iterations: 4) { index in
Swift.print("\(index)")
sleep((1..<3).randomElement()!) // Wait for n secs
}
onComplete()
}
}
Using concurrentPerform with async network processes
func downloadSync(path: String) -> (Data?, URLResponse?, Error?) {
var result: (Data?, URLResponse?, Error?)! = nil
let semaphore = DispatchSemaphore(value: 0)
let url = URL(string: path)!
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in // async operation
result = (data, response, error)
semaphore.signal()
}.resume()
semaphore.wait()
return result
}
let paths = ["http://qiita.com/mono0926/items/c32c008384df40bf4e41",
"http://qiita.com/mono0926/items/acef5cb3651620a355c3",
"http://qiita.com/mono0926/items/139014be6c15e32b9696"]
DispatchQueue.concurrentPerform(iterations: paths.count) { i in
let r = downloadSync(path: paths[i])
print(String(data: r.0!, encoding: .utf8))
}
Using concurrentPerform and DispatchGroup:
var storedError: NSError?
let downloadGroup = DispatchGroup()
let addresses = [PhotoURLString.overlyAttachedGirlfriend,
PhotoURLString.successKid,
PhotoURLString.lotsOfFaces]
let _ = DispatchQueue.global(qos: .userInitiated)
DispatchQueue.concurrentPerform(iterations: addresses.count) { index in
let address = addresses[index]
let url = URL(string: address)
downloadGroup.enter()
let photo = DownloadPhoto(url: url!) { _, error in
if error != nil {
storedError = error
}
downloadGroup.leave()
}
PhotoManager.shared.addPhoto(photo)
}
downloadGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
Correct way to specify the QoS is to dispatch the call to concurrentPerform to the desired queue:
https://developer.apple.com/videos/play/wwdc2017/706/?time=285
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.concurrentPerform(iterations: 3) { (i) in
...
}
}
Stride:
let lock = NSLock() // needed when accessing a variable from many threads
let stride = maxY / 20 // maybe set stride to num of cores?
let iterations = Int((Double(height) / Double(stride)).rounded(.up))
DispatchQueue.concurrentPerform(iterations: iterations) { i in
var subTotal = 0
let range = i * stride ..< min(maxY, (i + 1) * stride)
for y in range {
for x in 0 ..< maxX {
if ... {
subTotal += 1
}
}
}
lock.sync { count += subTotal } // needed when accessing a variable from many threads
}
Another example (untested):
- You should be able to update the total async in this example
var total = 0 let syncQueue = DispatchQueue(label: "...") // needed when accessing a variable from many threads DispatchQueue.concurrentPerform(iterations: maxY) { y in var subTotal = 0 for x in 0..<maxX { if ... { subTotal += 1 } } syncQueue.sync { // Needed when accessing a variable from many threads total += subTotal } } print(total)
Avoid creating too many threads
It might be tempting to create a lot of queues to gain better performance in your app. Unfortunately, creating threads comes with a cost and you should, therefore, avoid excessive thread creation. There are two common scenarios in which excessive thread creation occurs:
- Too many blocking tasks are added to concurrent queues forcing the system to create additional threads until the system runs out of threads for your app
- Too many private concurrent dispatch queues exist that all consume thread resources.
Gotchas:
- Sometimes doing concurrentPerform on the inner loop is more performant than on the outer loop
- Release build is faster than debug builds
- You can ⭐simulate release build speed⭐ by: setting optimizations turned off: xcode project target -> swift compiler optimizations: -> Debug -> optimizd for speed
- You can do any number of iterations with concurrentPerform:, only up to 8 threads will be scheduled.
- Always run concurrentPerform: in a Global queue.
Resources:
- A lot of info on concurrency in swift: https://www.uraimo.com/2017/05/07/all-about-concurrency-in-swift-1-the-present/
- Lots of info nuggets on concurrent performance: https://gist.github.com/FWEugene/3861f0460c3e23f684e113f0f8d6947f
- Create barrier: Using a barrier on a concurrent queue to synchronize writes https://www.avanderlee.com/swift/concurrent-serial-dispatchqueue/