Применение блоков длительного цикла
У меня есть следующий цикл в моем приложении
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
}
и радужное колесо показывает во время выполнения цикла. Как я могу управлять тем, что приложение все еще реагирует на действия пользовательского интерфейса? Примечание. У меня NSProgressIndicator обновлен в другой части кода, которая не обновляется (прогресс не отображается) во время выполнения цикла. У меня есть подозрение, что это как-то связано с рассылкой, но я довольно "зелен" с этим. Я ценю любую помощь. Благодарю.
1 ответ
Чтобы отправить что-то асинхронно, позвоните 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)))
}
}
concurrentPerform
(ранее известный как dispatch_apply
) будет выполнять различные итерации этого цикла одновременно, но автоматически оптимизирует количество одновременных потоков для возможностей вашего устройства. На моем MacBook Pro это сделало расчет в 4,8 раза быстрее, чем простой for
петля. Обратите внимание, я все еще отправляю все это в глобальную очередь (потому что concurrentPerform
выполняется синхронно, и мы никогда не хотим выполнять медленные синхронные вычисления в основном потоке), но concurrentPerform
будет выполнять расчеты параллельно. Это отличный способ насладиться параллелизмом в for
цикл таким образом, что вы не будете исчерпывать рабочие потоки GCD.
Кстати, вы упомянули, что вы обновляете NSProgressIndicator
, В идеале вы хотите обновлять его, когда обрабатывается каждый пиксель, но если вы это сделаете, пользовательский интерфейс может быть заблокирован и не справиться со всеми этими обновлениями. Вы в конечном итоге замедлите конечный результат, чтобы пользовательский интерфейс мог отслеживать все эти обновления индикатора прогресса.
Решение состоит в том, чтобы отделить обновление пользовательского интерфейса от обновлений прогресса. Вы хотите, чтобы фоновые вычисления информировали вас об обновлении каждого пикселя, но вы хотите, чтобы индикатор прогресса обновлялся, каждый раз фактически говоря: "Хорошо, обновите прогресс, сколько бы пикселей не было рассчитано с момента последней проверки". Для этого существуют громоздкие ручные методы, но 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)
}
}
Если вы сделаете это, он будет обновлять пользовательский интерфейс с максимально возможной частотой, но он никогда не будет переполнен очередью обновлений. Источник отправки объединит эти add
призывает вас.