Long cycle blocks application

巧了我就是萌 提交于 2019-11-26 12:33:13

问题


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.


回答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.



来源:https://stackoverflow.com/questions/39948082/long-cycle-blocks-application

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!