2

我的应用程序中有以下循环

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
}

循环执行期间显示彩虹轮。我如何管理该应用程序仍在响应 UI 操作?注意我在循环运行时未更新的代码的不同部分更新了 NSProgressIndicator(未显示进度)。我怀疑它与调度有关,但我对此很“绿色”。我很感激任何帮助。谢谢。

4

1 回答 1

6

要异步调度某些东西,请调用async适当的队列。例如,您可以更改此方法以在全局后台队列上进行计算,然后将结果报告回主队列。顺便说一句,当你这样做时,你会从立即返回结果转变为使用完成处理程序闭包,异步方法将在计算完成时调用该闭包:

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

你会这样称呼它:

// 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 推测您正在计算 Mandelbrot 集。如果是这样,将每个点的计算分派给全局队列并不是一个好主意(因为这些全局队列将它们的块分派给工作线程,但这些工作线程非常有限)。

如果您想避免用完所有这些全局队列工作线程,一个简单的选择是async从计算单个点的例程中取出调用,然后将遍历所有复杂值的整个例程分派到后台线:

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

这解决了“把它从主线程中取出”和“不要用完工作线程”的问题,但是现在我们已经从使用太多工作线程转变为只使用一个工作线程,而不是充分利用设备. 我们真的很想并行执行尽可能多的计算(同时不会耗尽工作线程)。

一种方法,在执行for复杂计算的循环时,是使用dispatch_apply(现在concurrentPerform在 Swift 3 中调用)。这就像一个for循环,但它相对于彼此同时执行每个循环(但最后,等待所有这些并发循环完成)。为此,请将外部for循环替换为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)))
    }
}

concurrentPerform以前称为dispatch_apply)将同时执行该循环的各种迭代,但它会自动优化并发线程数以适应您设备的功能。for在我的 MacBook Pro 上,这使得计算速度比简单循环快 4.8 倍。请注意,我仍然将整个事情分派到一个全局队列(因为concurrentPerform同步运行,我们不想在主线程上执行缓慢的同步计算),但concurrentPerform将并行运行计算。这是一种在循环中享受并发的好for方法,不会耗尽 GCD 工作线程。

曼德尔布罗塞特


顺便说一句,您提到您正在更新NSProgressIndicator. 理想情况下,您希望在处理每个像素时对其进行更新,但如果这样做,UI 可能会积压,无法跟上所有这些更新。您最终会减慢最终结果的速度,以使 UI 能够赶上所有这些进度指示器的更新。

解决方案是将 UI 更新与进度更新分离。您希望背景计算在更新每个像素时通知您,但您希望更新进度指示器,每次有效地说“好的,更新进度,无论我上次检查以来计算了多少像素”。有一些繁琐的手动技术可以做到这一点,但 GCD 提供了一个非常优雅的解决方案,一个调度源,或者更具体地说,一个DispatchSourceUserDataAdd.

所以定义调度源的属性和一个计数器来跟踪到目前为止已经处理了多少像素:

let source = DispatchSource.makeUserDataAddSource(queue: .main)
var pixelsProcessed: UInt = 0

然后为调度源设置一个事件处理程序,它会更新进度指示器:

source.setEventHandler() { [unowned self] in
    self.pixelsProcessed += self.source.data
    self.progressIndicator.doubleValue = Double(self.pixelsProcessed) / Double(width * height)
}
source.resume()

然后,当您处理像素时,您可以简单add地从后台线程获取您的源:

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

如果您这样做,它将以尽可能高的频率更新 UI,但它永远不会因更新队列而积压。调度源将为您合并这些add调用。

于 2016-10-09T22:45:02.650 回答