要异步分派某些内容,请调用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.
马丁推测您正在计算曼德尔布罗特集。如果是这样,将每个点的计算分派到全局队列并不是一个好主意(因为这些全局队列将它们的块分派到工作线程,但这些工作线程非常有限)。
如果您想避免用完所有这些全局队列工作线程,一个简单的选择是采用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
在斯威夫特 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)))
}
}
The concurrentPerform
(原名dispatch_apply
)将同时执行该循环的各种迭代,但它会根据设备的功能自动优化并发线程的数量。在我的 MacBook Pro 上,这使得计算速度比简单的计算快 4.8 倍for
环形。注意,我仍然将整个事情分派到全局队列(因为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
呼唤你。