2

I hve following cycle in my app

var maxIterations: Int = 0

func calculatePoint(cn: Complex) -> Int {

    let threshold: Double = 2
    var z: Complex = .init(re: 0, im: 0)
    var z2: Complex = .init(re: 0, im: 0)
    var iteration: Int = 0

    repeat {
        z2 = self.pow2ForComplex(cn: z)
        z.re = z2.re + cn.re
        z.im = z2.im + cn.im
        iteration += 1
    } while self.absForComplex(cn: z) <= threshold && iteration < self.maxIterations

    return iteration
}

and rainbow wheel is showing during the cycle execution. How I can manage that app is still responding to UI actions? Note I have NSProgressIndicator updated in different part of code which is not being updated (progress is not shown) while the cycle is running. I have suspicion that it has something to do with dispatcing but I'm quite "green" with that. I do appreciate any help. Thanks.

Dawy
  • 770
  • 6
  • 23
  • 1
    You need to run the long calculation in the background. – rmaddy Oct 09 '16 at 20:54
  • Completely unrelated, but I might suggest moving some of the `Complex` calculations into your `struct`/`class`, and you then end up with a more intuitive Mandelbrot algorithm: https://gist.github.com/robertmryan/91536bf75e46cbdaed92c37e99fdbe7d – Rob Oct 10 '16 at 10:33

1 Answers1

6

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.

mandelbrotset


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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 2
    It seems that OP wants to calculate/draw the "Mandelbrot set" M, and then only the number of iterations is needed, not the final value. A point c belongs to M if the orbit of z=0 under iterations of f(z)=z^2+c stays bounded. The number of iterations required to leave a certain disk is often used to colour the complement of M. – Martin R Oct 10 '16 at 05:28
  • @MartinR - Ah, I'm sure you're right. But in that case, dispatching each calculation to a global thread is probably not a good idea, because he may well exhaust all of the available the worker threads. It's probably better to move the concurrency up a level, and use `dispatch_apply` (aka `concurrentPerform` in Swift 3) to enjoy a degree of concurrency. I've updated my answer accordingly. – Rob Oct 10 '16 at 10:28
  • 1
    Very nice (but I can upvote only once :) – Btw, `dispatch_apply` had a `queue` parameter. To which queue does `concurrentPerform` dispatch? These questions http://stackoverflow.com/questions/39590128/what-is-equivalent-of-dispatch-apply-in-swift-3, http://stackoverflow.com/questions/39590897/swift-3-conversion do not answer that clearly (in my opinion), but perhaps I am overlooking something. – Martin R Oct 10 '16 at 11:03
  • 2
    @MartinR - Looking [at the source](https://github.com/apple/swift-corelibs-libdispatch/blob/d04a0dff9f0ecf327d9fc7626f3013be4d9353f5/src/swift/Queue.swift#L106) for `concurrentPerform` and tracing all the way back to where it calls `dispatch_apply`, it uses "the root queue for the current thread ... (i.e. one of the global concurrent queues or a queue created with `dispatch_pthread_root_queue_create()`). If there is no such queue, the default priority global concurrent queue will be used." – Rob Oct 10 '16 at 16:19
  • Thanks a lot for your thorough suggestions. I'll play with that and will be back with my results. – Dawy Oct 11 '16 at 20:04
  • I played a bit ith dispatch things in another project and it worked fine. My issue is with incorporate it into graphics handling. Would you be so kind and help me wth context creation and passing bitmab content via makeImage()? – Dawy Oct 15 '16 at 15:39
  • I updated [my gist](https://gist.github.com/robertmryan/91536bf75e46cbdaed92c37e99fdbe7d) with a little more code. – Rob Oct 15 '16 at 16:47
  • Thanks, this is very useful and clear. Regarding the `makeUserDataAddSource`, you're 100% right that the UI update thread "will never get backlogged with a queue of updates." However, it's weird to note that the event handler on the source might get called a LOT more often than 60 times per second. Using `CADisplayLink` with `preferredFramesPerSecond` set to `0` is another way to go, but I guess it could bog down the Main Runloop... – Dan Rosenstark May 08 '17 at 19:56