Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
385 views
in Technique[技术] by (71.8m points)

swift3 - Long cycle blocks application

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...