长周期块应用

Daw*_*awy 2 nsprogressindicator dispatch-async swift3

我在我的应用程序中遵循以下周期

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
}
Run Code Online (Sandbox Code Playgroud)

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

Rob*_*Rob 5

要异步分发某些内容,请调用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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以这样称呼它:

// 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.
Run Code Online (Sandbox Code Playgroud)

马丁推测您正在计算曼德布罗集。如果是这样,将每个点的计算分配给全局队列不是一个好主意(因为这些全局队列将其块分配给工作线程,但是这些工作线程非常有限)。

如果要避免用完所有这些全局队列工作器线程,一个简单的选择是将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)))
    }
}
Run Code Online (Sandbox Code Playgroud)

那解决了“摆脱主线程”和“不占用工作线程”的问题,但是现在我们已经从使用太多工作线程转变为仅使用一个工作线程,而没有充分利用设备。我们确实希望并行执行尽可能多的计算(同时不耗尽工作线程)。

在进行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)))
    }
}
Run Code Online (Sandbox Code Playgroud)

concurrentPerform(原名dispatch_apply)将执行循环的各种迭代的同时,却会自动优化的并发线程数为您的设备的能力。在我的MacBook Pro上,这比简单for循环快了4.8倍。注意,我仍然将整个事件分配到全局队列中(因为concurrentPerform它是同步运行的,并且我们从不希望在主线程上执行缓慢的同步计算),但是concurrentPerform会并行运行这些计算。这是在for循环中享受并发性的好方法,这样您就不会耗尽GCD工作线程。

mandelbrotset


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

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

因此,请为调度源和计数器定义属性,以跟踪到目前为止已处理了多少像素:

let source = DispatchSource.makeUserDataAddSource(queue: .main)
var pixelsProcessed: UInt = 0
Run Code Online (Sandbox Code Playgroud)

然后为调度源设置事件处理程序,该事件处理程序将更新进度指示器:

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

然后,在处理像素时,您可以简单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)
    }
}
Run Code Online (Sandbox Code Playgroud)

如果执行此操作,它将以最大可能的频率更新UI,但绝不会积压更新队列。调度源将为您合并这些add呼叫。

  • 看来OP想要计算/绘制“ Mandelbrot集” M,然后仅需要迭代次数,而不是最终值。如果在f(z)= z ^ 2 + c的迭代中z = 0的轨道保持有界,则点c属于M。离开某个磁盘所需的迭代次数通常用于为M的补数着色。 (2认同)
  • @MartinR-在[源代码](https://github.com/apple/swift-corelibs-libdispatch/blob/d04a0dff9f0ecf327d9fc7626f3013be4d9353f5/src/swift/Queue.swift#L106)进行查找并一直追溯到它调用`dispatch_apply`的位置,它将使用“当前线程的根队列...(即全局并发队列之一或使用`dispatch_pthread_root_queue_create()创建的队列)。如果没有此类队列,则默认为将使用优先级全局并发队列。” (2认同)