¿Valores máximos del semáforo?

Nov 08 2020

Por ejemplo, hay un ciclo de 1000 tiempos. ¿Cuál es el valor máximo para hacerlo rápido, efectivo y no conducir a un punto muerto?

let group = DispatchGroup()
let queue = DispatchQueue(label: "com.num.loop", attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 4)
for i in 1...1000 {
    semaphore.wait()
    group.enter()
    queue.async(group: group, execute: {
        doWork(i)                                    
        group.leave()
        semaphore.signal()
    })            
}

group.notify(queue: DispatchQueue.main) {
    // go on...
}

Respuestas

4 Rob Nov 08 2020 at 22:16

Un par de observaciones:

  1. Nunca querrá exceder la cantidad máxima de subprocesos de trabajo de GCD por QoS. Si excede esto, puede experimentar bloqueo dentro de su aplicación. La última vez que verifiqué, este límite era de 64 hilos.

  2. Habiendo dicho eso, generalmente hay poco beneficio en exceder la cantidad de núcleos en su dispositivo.

  3. A menudo, dejamos que GCD calcule el número máximo de subprocesos simultáneos que podemos usar concurrentPerform, que se optimiza automáticamente para el dispositivo. También elimina la necesidad de semáforos o grupos, lo que a menudo conduce a un código menos desordenado:

    DispatchQueue.global().async {
        DispatchQueue.concurrentPerform(iterations: 1000) { i in
            doWork(i)                                    
        }
    
        DispatchQueue.main.async {
            // go on...
        }
    }
    

    El concurrentPerformejecutará las 1,000 iteraciones en paralelo, pero limitando el número de subprocesos concurrentes a un nivel apropiado para su dispositivo, eliminando la necesidad del semáforo. Pero concurrentPerform, en sí mismo, es sincrónico, no procede hasta que se realizan todas las iteraciones, lo que elimina la necesidad del grupo de despacho. Entonces, envíe todo concurrentPerforma una cola en segundo plano, y cuando esté listo, simplemente ejecute su "código de finalización" (o, en su caso, envíe ese código de regreso a la cola principal).

  4. Si bien lo he defendido concurrentPerformanteriormente, eso solo funciona si doWorkrealiza su tarea de forma sincrónica (por ejemplo, alguna operación de cálculo). Si está iniciando algo que es, en sí mismo, asincrónico, entonces tenemos que recurrir a esta técnica de semáforo / grupo. (O, quizás mejor, use Operationsubclases asincrónicas con una cola con límite razonable maxConcurrentOperationCounto Combinar flatMap(maxPublishers:_:)con límite razonable en el recuento).

    Con respecto al valor de umbral razonable en este caso, no hay un número mágico. Solo tiene que realizar algunas pruebas empíricas para encontrar un equilibrio razonable entre la cantidad de núcleos y lo que podría estar sucediendo dentro de su aplicación. Por ejemplo, para las solicitudes de red, a menudo usamos 4 o 6 como un recuento máximo, no solo considerando el beneficio disminuido de exceder ese recuento, sino también las implicaciones del impacto en nuestro servidor si miles de usuarios envían demasiados mensajes simultáneos. solicitudes al mismo tiempo.

  5. En términos de "hacerlo rápido", la elección de "cuántas iteraciones se deben permitir que se ejecuten al mismo tiempo" es solo una parte del proceso de toma de decisiones. El problema más crítico se convierte rápidamente en asegurarse de que doWorkfuncione lo suficiente para justificar la modesta sobrecarga introducida por el patrón concurrente.

    Por ejemplo, si procesa una imagen de 1.000 × 1.000 píxeles, puede realizar 1.000.000 de iteraciones, cada una de las cuales procesa un píxel. Pero si lo hace, es posible que descubra que en realidad es más lento que su interpretación no concurrente. En cambio, es posible que tenga 1000 iteraciones, cada una de las cuales procesa 1000 píxeles. O puede tener 100 iteraciones, cada una procesando 10,000 píxeles. Esta técnica, llamada "zancadas", a menudo requiere un poco de investigación empírica para encontrar el equilibrio correcto entre cuántas iteraciones se realizarán y cuánto trabajo se realiza en cada una. (Y, por cierto, a menudo este patrón de zancadas también puede evitar la rotura de caché, un escenario que puede surgir si varios subprocesos compiten por direcciones de memoria adyacentes).

  6. En relación con el punto anterior, a menudo queremos que estos subprocesos sincronicen su acceso a los recursos compartidos (para mantenerlos seguros para los subprocesos). Esa sincronización puede introducir contención entre estos hilos. Entonces querrá pensar en cómo y cuándo realiza esta sincronización.

    Por ejemplo, en lugar de tener varias sincronizaciones dentro doWork, es posible que cada iteración actualice una variable local (donde no se necesita sincronización) y realice la actualización sincronizada del recurso compartido solo cuando se realicen los cálculos locales. Es difícil responder a esta pregunta en abstracto, ya que dependerá en gran medida de lo que se doWorkesté haciendo, pero puede afectar fácilmente el desempeño general.