(
Nota: el autor del material original es Thomas @tclementdev, un usuario de github y twitter . La narración en primera persona utilizada por el autor se guarda en la traducción a continuación )
.Creo que la mayoría de los desarrolladores usan libdispatch de manera ineficiente debido a cómo se introdujo en la comunidad, así como debido a la confusa documentación y API. Llegué a este pensamiento después de leer la discusión sobre "concurrencia" en la lista de correo de desarrollo de Swift (swift-evolution). Los mensajes de Pierre Habouzit (Pierre Habouzit - está comprometido con el apoyo de libdispatch en Apple) son especialmente ilustrados:
También tiene muchos tweets sobre este tema:
Hecho por mi:
- El programa debe tener muy pocas colas usando el grupo global ( hilos - aprox. Por. ). Si todas estas colas están activas simultáneamente, recibirá el mismo número de subprocesos que se ejecutan simultáneamente. Estas colas deben considerarse como contextos de ejecución en el programa (GUI, almacenamiento, trabajo en segundo plano, ...) que se benefician de la ejecución concurrente.
- Comience con la ejecución secuencial. Cuando descubra un problema de rendimiento, tome medidas para descubrir la causa. Y si la ejecución paralela ayuda, úsela con cuidado. Siempre verifique que el código paralelo funcione bajo presión del sistema. Por defecto, reutilice las colas. Agregue líneas cuando traiga beneficios medibles. La mayoría de las aplicaciones no deben usar más de tres o cuatro colas.
- Las colas que tienen otra cola establecida como destino funcionan bien y escalan.
( Tenga en cuenta perev .: acerca de cómo establecer una cola como destino para otra cola se puede leer, por ejemplo, aquí ) . - No use dispatch_get_global_queue (). Esto no es compatible con la calidad del servicio y las prioridades y puede conducir a un crecimiento explosivo en el número de flujos. En su lugar, ejecute su código en uno de sus contextos de ejecución.
- dispatch_async () es un desperdicio de recursos para pequeños bloques ejecutables (<1 ms), ya que esta llamada probablemente requerirá la creación de un nuevo hilo debido al celo excesivo de libdispatch. En lugar de cambiar el contexto de ejecución para proteger el estado compartido, use mecanismos de bloqueo para acceder al estado compartido al mismo tiempo.
- Algunas clases / bibliotecas están bien diseñadas porque reutilizan el contexto de ejecución que el código de llamada les pasa. Esto permite el uso de un bloqueo convencional para garantizar la seguridad de la rosca. os_unfair_lock suele ser el mecanismo de bloqueo más rápido del sistema: funciona mejor con las prioridades y provoca menos cambios de contexto.
- En el caso de la ejecución paralela, sus tareas no deberían tener problemas entre sí, de lo contrario la productividad caerá bruscamente. Pelear toma muchas formas. El caso obvio: la lucha por capturar la cerradura. Pero en realidad, tal lucha no significa nada más que usar un recurso compartido, que se convierte en un cuello de botella: IPC (comunicación entre procesos) / demonios del sistema operativo, malloc (bloqueo), memoria compartida, E / S.
- No necesita todo el código para ejecutarse de forma asincrónica para evitar un aumento explosivo en el número de hilos. Es mucho mejor usar un número limitado de colas inferiores y negarse a usar dispatch_get_global_queue ().
( Nota perev. 1: aparentemente, este es un caso en el que se produce un aumento explosivo en la cantidad de hilos al sincronizar una gran cantidad de tareas paralelas "Si tengo muchos bloques y todos quieren esperar, podemos obtener lo que llamamos hilo" explosión " )
( Nota p. 2: de la discusión se puede entender que Pierre Habuzit significa las colas "que el núcleo conoce cuando tienen tareas" en las colas inferiores. Aquí estamos hablando del núcleo del sistema operativo ) . - No debemos olvidar la complejidad y los errores que surgen en una arquitectura llena de ejecuciones asíncronas y devoluciones de llamadas. El código secuencialmente ejecutable sigue siendo mucho más fácil de leer, escribir y mantener.
- Las colas competitivas están menos optimizadas que las secuenciales. Úselos si está midiendo ganancias de rendimiento, de lo contrario es una optimización prematura.
- Si necesita enviar tareas en una cola de forma asíncrona y sincrónica, entonces, en lugar de dispatch_sync (), utilice dispatch_async_and_wait (). dispatch_async_and_wait () no garantiza la ejecución en el hilo desde el que se originó la llamada, lo que reduce el cambio de contexto cuando la cola de destino está activa.
( Nota transl. 1: realmente dispatch_sync () tampoco garantiza, la documentación al respecto solo dice "ejecuta un bloque en el hilo actual, siempre que sea posible. Con una excepción: el bloque enviado a la cola principal siempre se ejecuta en el hilo principal. " )
( Nota transl. 2: sobre dispatch_async_and_wait () en la documentación y en el código fuente ) - Usar correctamente 3-4 núcleos no es tan simple. La mayoría de los que lo intentan, de hecho, no pueden hacer frente al escalado y desperdiciar energía en aras de un pequeño aumento en la productividad. Cómo funcionan los procesadores con sobrecalentamiento no ayudará. Por ejemplo, Intel deshabilitará Turbo-Boost si se usan suficientes núcleos.
- Mida el rendimiento de su producto en el mundo real para asegurarse de hacerlo más rápido, no más lento. Tenga cuidado con las pruebas de micro rendimiento: ocultan la influencia del caché y mantienen caliente el grupo de subprocesos. Para verificar lo que está haciendo, siempre debe tener una prueba de macro.
- libdispatch es efectivo, pero no hay milagros. Los recursos no son infinitos. No puede ignorar la realidad del sistema operativo y el hardware en el que se ejecuta el código. Además, no todos los códigos están bien paralelizados.
Eche un vistazo a todas las llamadas dispatch_async () en su código y pregúntese: la tarea que envía con esta llamada realmente vale el cambio de contexto. En la mayoría de los casos, el bloqueo es probablemente la mejor opción.
Tan pronto como comience a usar y reutilizar colas (contextos de ejecución) desde un conjunto prediseñado, habrá peligro de puntos muertos. El peligro surge cuando se envían tareas a estas colas usando dispatch_sync (). Esto suele suceder cuando se utilizan colas para la seguridad de subprocesos. Entonces, de nuevo: la solución es usar mecanismos de bloqueo y usar dispatch_async () solo cuando necesite cambiar a otro contexto de ejecución.
Personalmente, vi grandes mejoras en el rendimiento al seguir estas pautas.
(en programas altamente cargados). Este es un nuevo enfoque, pero vale la pena.
Más enlaces
El programa debe tener muy pocas colas usando el grupo global
.<...>
Entiendo que es difícil para mí transmitir mi punto de vista, porque no soy un chico en arquitectura de lenguaje, soy un chico en arquitectura de sistemas. Y definitivamente no entiendo a los actores lo suficiente como para decidir cómo integrarlos en el sistema operativo. Pero para mí, volviendo al ejemplo de la base de datos, los Actor-Database-Data, o la Actor-Network-Interface de la correspondencia anterior, son diferentes de, por ejemplo, esta consulta SQL o esta consulta de red. Las primeras son las entidades que el sistema operativo debe conocer en el núcleo. Mientras que una consulta SQL o una consulta de red son solo actores en cola para su ejecución en primer lugar. En otras palabras, estos actores de nivel superior son diferentes porque son de nivel superior, directamente encima del núcleo / tiempo de ejecución de bajo nivel. Y esta es la esencia sobre la que el núcleo debería poder razonar. Esto los hace geniales.
Hay 2 tipos de colas y el nivel de API correspondiente en la biblioteca de despacho:
- colas globales que no son colas como las demás. Y en realidad, son solo una abstracción sobre el grupo de subprocesos.
- todas las demás colas que puede establecer como objetivo una para la otra como desee.
Hoy ha quedado claro que esto fue un error y que debería haber 3 tipos de colas:
- colas globales, que no son colas reales, pero representan qué familia de atributos del sistema requiere su contexto de ejecución (principalmente prioridades). Y debemos prohibir el envío de tareas directamente a estas colas.
- colas inferiores (que GCD ha rastreado en los últimos años y denomina "bases" en el código fuente ( parece que el código fuente de GCD se refiere a - aprox. transl. ). Las colas inferiores son conocidas por el núcleo cuando tienen tareas.
- cualquier otra cola "interna" que el núcleo desconozca en absoluto.
En el grupo de desarrollo de despacho, lamentamos cada día que pasa que la diferencia entre el segundo y el tercer grupo de colas no se aclaró inicialmente en la API.
Me gusta llamar al segundo grupo "contextos de ejecución", pero puedo entender por qué quieres llamarlos actores. Esto es quizás más consistente (y el GCD hizo lo mismo, presentando esto y aquello como una cola). Tales "Actores" de nivel superior deberían ser pocos porque si todos se activan al mismo tiempo, necesitarán la misma cantidad de hilos en el proceso. Y este no es un recurso que pueda escalarse. Por eso es importante distinguir entre ellos. Y, como discutimos, también se usan comúnmente para proteger un estado, recurso o similar compartido. Puede que no sea posible hacer esto utilizando actores internos.
<...>
Comience con la ejecución secuencial.
No use colas globales
Cuidado con las líneas competitivas
No use llamadas asíncronas para proteger el estado compartido
No use llamadas asíncronas para tareas pequeñas
Algunas clases / bibliotecas deberían ser sincrónicas
La lucha de tareas paralelas entre ellos es un asesino de la productividad.
Para evitar puntos muertos, use mecanismos de bloqueo cuando necesite proteger un estado compartido
No use semáforos para esperar una tarea asincrónica
La API de NSOperation tiene algunas dificultades serias que pueden conducir a la degradación del rendimiento.
Evite las pruebas de micro rendimiento
Los recursos no son ilimitados.
Acerca de dispatch_async_and_wait ()
Usar 3-4 núcleos no es fácil
Se han logrado muchas mejoras de rendimiento en iOS 12 con demonios de un solo subproceso