Long cycle blocks application
Solution 1:
To dispatch something asynchronously, call async
on the appropriate queue. For example, you might change this method to do the calculation on a global background queue, and then report the result back on the main queue. By the way, when you do that, you shift from returning the result immediately to using a completion handler closure which the asynchronous method will call when the calculation is done:
func calculatePoint(_ cn: Complex, completionHandler: @escaping (Int) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
// do your complicated calculation here which calculates `iteration`
DispatchQueue.main.async {
completionHandler(iteration)
}
}
}
And you'd call it like so:
// start NSProgressIndicator here
calculatePoint(point) { iterations in
// use iterations here, noting that this is called asynchronously (i.e. later)
// stop NSProgressIndicator here
}
// don't use iterations here, because the above closure is likely not yet done by the time we get here;
// we'll get here almost immediately, but the above completion handler is called when the asynchronous
// calculation is done.
Martin has surmised that you are calculating a Mandelbrot set. If so, dispatching the calculation of each point to a global queue is not a good idea (because these global queues dispatch their blocks to worker threads, but those worker threads are quite limited).
If you want to avoid using up all of these global queue worker threads, one simple choice is to take the async
call out of your routine that calculates an individual point, and just dispatch the whole routine that iterates through all of the complex values to a background thread:
DispatchQueue.global(qos: .userInitiated).async {
for row in 0 ..< height {
for column in 0 ..< width {
let c = ...
let m = self.mandelbrotValue(c)
pixelBuffer[row * width + column] = self.color(for: m)
}
}
let outputCGImage = context.makeImage()!
DispatchQueue.main.async {
completionHandler(NSImage(cgImage: outputCGImage, size: NSSize(width: width, height: height)))
}
}
That's solves the "get it off the main thread" and the "don't use up the worker threads" problems, but now we've swung from using too many worker threads, to only using one worker thread, not fully utilizing the device. We really want to do as many calculations in parallel (while not exhausting the worker threads).
One approach, when doing a for
loop for complex calculations, is to use dispatch_apply
(now called concurrentPerform
in Swift 3). This is like a for
loop, but it does the each of the loops concurrently with respect to each other (but, at the end, waits for all of those concurrent loops to finish). To do this, replace the outer for
loop with concurrentPerform
:
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.concurrentPerform(iterations: height) { row in
for column in 0 ..< width {
let c = ...
let m = self.mandelbrotValue(c)
pixelBuffer[row * width + column] = self.color(for: m)
}
}
let outputCGImage = context.makeImage()!
DispatchQueue.main.async {
completionHandler(NSImage(cgImage: outputCGImage, size: NSSize(width: width, height: height)))
}
}
The concurrentPerform
(formerly known as dispatch_apply
) will perform the various iterations of that loop concurrently, but it will automatically optimize the number of concurrent threads for the capabilities of your device. On my MacBook Pro, this made the calculation 4.8 times faster than the simple for
loop. Note, I still dispatch the whole thing to a global queue (because concurrentPerform
runs synchronously, and we never want to perform slow, synchronous calculations on the main thread), but concurrentPerform
will run the calculations in parallel. It's a great way to enjoy concurrency in a for
loop in such a way that you won't exhaust GCD worker threads.
By the way, you mentioned that you are updating a NSProgressIndicator
. Ideally, you want to update it as every pixel is processed, but if you do that, the UI may get backlogged, unable to keep up with all of these updates. You'll end up slowing the final result to allow the UI to catch up to all of those progress indicator updates.
The solution is to decouple the UI update from the progress updates. You want the background calculations to inform you as each pixel is updated, but you want the progress indicator to be updated, each time effectively saying "ok, update the progress with however many pixels were calculated since the last time I checked". There are cumbersome manual techniques to do that, but GCD provides a really elegant solution, a dispatch source, or more specifically, a DispatchSourceUserDataAdd
.
So define properties for the dispatch source and a counter to keep track of how many pixels have been processed thus far:
let source = DispatchSource.makeUserDataAddSource(queue: .main)
var pixelsProcessed: UInt = 0
And then set up an event handler for the dispatch source, which updates the progress indicator:
source.setEventHandler() { [unowned self] in
self.pixelsProcessed += self.source.data
self.progressIndicator.doubleValue = Double(self.pixelsProcessed) / Double(width * height)
}
source.resume()
And then, as you process the pixels, you can simply add
to your source from the background thread:
DispatchQueue.concurrentPerform(iterations: height) { row in
for column in 0 ..< width {
let c = ...
let m = self.mandelbrotValue(for: c)
pixelBuffer[row * width + column] = self.color(for: m)
self.source.add(data: 1)
}
}
If you do this, it will update the UI with the greatest frequency possible, but it will never get backlogged with a queue of updates. The dispatch source will coalesce these add
calls for you.