Применение блоков с длинным циклом

Я следую за циклом в моем приложении

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 обновлен в другой части кода, который не обновляется (прогресс не отображается) во время выполнения цикла. У меня есть подозрение, что это как-то связано с диспетчеризацией, но я вполне «зеленый» с этим. Я ценю любую помощь. Спасибо.


person Dawy    schedule 09.10.2016    source источник
comment
Вам нужно запустить долгий расчет в фоновом режиме.   -  person rmaddy    schedule 09.10.2016
comment
Совершенно не связано, но я мог бы предложить перенести некоторые Complex вычисления в ваш _2 _ / _ 3_, и тогда вы получите более интуитивный алгоритм Мандельброта: gist.github.com/robertmryan/91536bf75e46cbdaed92c37e99fdbe7d   -  person Rob    schedule 10.10.2016


Ответы (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 в 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) будет выполнять различные итерации этого цикла одновременно, но он автоматически оптимизирует количество параллельных потоков для возможностей вашего устройства. На моем 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 звонки за вас.

person Rob    schedule 09.10.2016
comment
Кажется, что OP хочет вычислить / нарисовать множество Мандельброта M, и тогда требуется только количество итераций, а не окончательное значение. Точка c принадлежит M, если орбита точки z = 0 при итерациях f (z) = z ^ 2 + c остается ограниченной. Количество итераций, необходимых для выхода из определенного диска, часто используется для раскрашивания дополнения M. - person Martin R; 10.10.2016
comment
@MartinR - Ах, я уверен, ты прав. Но в этом случае отправка каждого вычисления в глобальный поток, вероятно, не является хорошей идеей, потому что он вполне может исчерпать все доступные рабочие потоки. Вероятно, лучше переместить параллелизм на более высокий уровень и использовать dispatch_apply (также известный как concurrentPerform в Swift 3), чтобы получить определенную степень параллелизма. Я соответствующим образом обновил свой ответ. - person Rob; 10.10.2016
comment
Очень красиво (но я могу проголосовать только один раз :) - Кстати, у dispatch_apply был параметр queue. В какую очередь отправляется concurrentPerform? Эти вопросы stackoverflow.com/questions/39590128/, stackoverflow.com/questions/39590897/swift-3- конверсия не отвечаю на это однозначно (на мой взгляд), но, возможно, я что-то упускаю. - person Martin R; 10.10.2016
comment
@MartinR - Ищем - person Rob; 10.10.2016
comment
Большое спасибо за ваши подробные предложения. Я поиграю с этим и вернусь со своими результатами. - person Dawy; 11.10.2016
comment
Я немного поигрался с диспетчером в другом проекте, и он работал нормально. Моя проблема заключается в том, чтобы включить его в обработку графики. Не могли бы вы помочь мне с созданием контекста и передачей растрового содержимого через makeImage ()? - person Dawy; 15.10.2016
comment
Я обновил суть, добавив немного дополнительного кода. - person Rob; 15.10.2016
comment
Спасибо, это очень полезно и понятно. Что касается makeUserDataAddSource, вы на 100% правы в том, что поток обновления пользовательского интерфейса никогда не будет задерживаться очередью обновлений. Однако странно отметить, что обработчик событий в источнике может вызывать LOT чаще, чем 60 раз в секунду. Использование CADisplayLink с preferredFramesPerSecond, установленным на 0, - это еще один способ пойти, но я думаю, это может затормозить основной цикл выполнения ... - person Dan Rosenstark; 08.05.2017