Concurrent perform


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))
}

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()

let stride = maxY / 20
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 }
}

Another example (untested):

var total = 0

let syncQueue = DispatchQueue(label: "...")

DispatchQueue.concurrentPerform(iterations: maxY) { y in
    var subTotal = 0
    for x in 0..<maxX {
        if ... {
            subTotal += 1
        }
    }
    syncQueue.sync {
        total += subTotal
    }
}

print(total)

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:

  • Alot of info on concurrency in swift: https://www.uraimo.com/2017/05/07/all-about-concurrency-in-swift-1-the-present/