Primero, es importante conocer la diferencia entre subprocesos y colas y lo que realmente hace GCD. Cuando usamos colas de despacho (a través de GCD), realmente estamos haciendo cola, no subprocesos. El marco de Dispatch fue diseñado específicamente para alejarnos del subproceso, ya que Apple admite que "implementar una solución de subprocesamiento correcta [puede] volverse extremadamente difícil, si no [a veces] imposible de lograr". Por lo tanto, para realizar tareas al mismo tiempo (tareas que no queremos que congelen la interfaz de usuario), todo lo que tenemos que hacer es crear una cola de esas tareas y entregársela a GCD. Y GCD maneja todos los subprocesos asociados. Por lo tanto, todo lo que realmente estamos haciendo es hacer cola.
Lo segundo que hay que saber de inmediato es qué es una tarea. Una tarea es todo el código dentro de ese bloque de cola (no dentro de la cola, porque podemos agregar cosas a una cola todo el tiempo, pero dentro del cierre donde lo agregamos a la cola). En ocasiones, una tarea se denomina bloque y, a veces, un bloqueo se denomina tarea (pero se conocen más comúnmente como tareas, especialmente en la comunidad Swift). Y no importa cuánto o poco código, todo el código dentro de las llaves se considera una sola tarea:
serialQueue.async {
// this is one task
// it can be any number of lines with any number of methods
}
serialQueue.async {
// this is another task added to the same queue
// this queue now has two tasks
}
Y es obvio mencionar que concurrente simplemente significa al mismo tiempo con otras cosas y en serie significa uno tras otro (nunca al mismo tiempo). Serializar algo, o poner algo en serie, solo significa ejecutarlo de principio a fin en su orden de izquierda a derecha, de arriba a abajo, sin interrupciones.
Hay dos tipos de colas, seriales y concurrentes, pero todas las colas son concurrentes entre sí . El hecho de que desee ejecutar cualquier código "en segundo plano" significa que desea ejecutarlo simultáneamente con otro hilo (normalmente el hilo principal). Por lo tanto, todas las colas de despacho, en serie o simultáneas, ejecutan sus tareas al mismo tiempo que otras colas . Cualquier serialización realizada por colas (por colas en serie), solo tiene que ver con las tareas dentro de esa única cola de despacho [serial] (como en el ejemplo anterior donde hay dos tareas dentro de la misma cola serial; esas tareas se ejecutarán una después de el otro, nunca simultáneamente).
LAS COLAS DE SERIE (a menudo conocidas como colas de despacho privadas) garantizan la ejecución de las tareas de una en una, de principio a fin, en el orden en que se agregaron a esa cola específica. Esta es la única garantía de serialización en cualquier parte del debate sobre las colas de envío.- que las tareas específicas dentro de una cola de serie específica se ejecutan en serie. Sin embargo, las colas seriales pueden ejecutarse simultáneamente con otras colas seriales si son colas separadas porque, nuevamente, todas las colas son concurrentes entre sí. Todas las tareas se ejecutan en subprocesos distintos, pero no se garantiza que todas las tareas se ejecuten en el mismo subproceso (no es importante, pero es interesante saberlo). Y el marco de iOS no viene con colas seriales listas para usar, debe crearlas. Las colas privadas (no globales) son seriales por defecto, así que para crear una cola serial:
let serialQueue = DispatchQueue(label: "serial")
Puede hacerlo concurrente a través de su propiedad de atributo:
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])
Pero en este punto, si no está agregando ningún otro atributo a la cola privada, Apple recomienda que solo use una de sus colas globales listas para usar (que son todas concurrentes). En la parte inferior de esta respuesta, verá otra forma de crear colas en serie (utilizando la propiedad de destino), que es como Apple recomienda hacerlo (para una gestión de recursos más eficiente). Pero por ahora, etiquetarlo es suficiente.
LAS COLAS CONCURRENTES (a menudo conocidas como colas de despacho global) pueden ejecutar tareas simultáneamente; Sin embargo, se garantiza que las tareas se inicien en el orden en que se agregaron a esa cola específica, pero a diferencia de las colas en serie, la cola no espera a que finalice la primera tarea antes de comenzar la segunda. Las tareas (como con las colas en serie) se ejecutan en subprocesos distintos y (como con las colas en serie) no se garantiza que todas las tareas se ejecuten en el mismo subproceso (no es importante, pero es interesante saberlo). Y el marco de iOS viene con cuatro colas simultáneas listas para usar. Puede crear una cola simultánea usando el ejemplo anterior o usando una de las colas globales de Apple (que generalmente se recomienda):
let concurrentQueue = DispatchQueue.global(qos: .default)
RESISTENTE AL CICLO DE RETENCIÓN: las colas de envío son objetos contados por referencias, pero no es necesario retener y liberar colas globales porque son globales y, por lo tanto, se ignora la retención y liberación. Puede acceder a las colas globales directamente sin tener que asignarlas a una propiedad.
Hay dos formas de distribuir colas: de forma sincrónica y asincrónica.
SYNC DISPATCHING significa que el subproceso donde se envió la cola (el subproceso que llama) se detiene después de enviar la cola y espera a que la tarea en ese bloque de cola termine de ejecutarse antes de reanudar. Para enviar sincrónicamente:
DispatchQueue.global(qos: .default).sync {
// task goes in here
}
ASYNC DISPATCHING significa que el subproceso que llama continúa ejecutándose después de enviar la cola y no espera a que la tarea en ese bloque de cola termine de ejecutarse. Para enviar de forma asincrónica:
DispatchQueue.global(qos: .default).async {
// task goes in here
}
Ahora, uno podría pensar que para ejecutar una tarea en serie, se debe usar una cola de serie, y eso no es exactamente correcto. Para ejecutar múltiples tareas en serie, se debe usar una cola de serie, pero todas las tareas (aisladas por sí mismas) se ejecutan en serie. Considere este ejemplo:
whichQueueShouldIUse.syncOrAsync {
for i in 1...10 {
print(i)
}
for i in 1...10 {
print(i + 100)
}
for i in 1...10 {
print(i + 1000)
}
}
No importa cómo configure (en serie o concurrente) o envíe (sincronizada o asincrónica) esta cola, esta tarea siempre se ejecutará en serie. El tercer ciclo nunca se ejecutará antes del segundo ciclo y el segundo ciclo nunca se ejecutará antes del primer ciclo. Esto es cierto en cualquier cola que utilice cualquier envío. Es cuando introduces múltiples tareas y / o colas donde la serie y la concurrencia realmente entran en juego.
Considere estas dos colas, una en serie y otra simultánea:
let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue.global(qos: .default)
Digamos que enviamos dos colas simultáneas en asincrónico:
concurrentQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
103
3
104
4
105
5
Su salida está desordenada (como se esperaba) pero observe que cada cola ejecuta su propia tarea en serie. Este es el ejemplo más básico de simultaneidad: dos tareas que se ejecutan al mismo tiempo en segundo plano en la misma cola. Ahora hagamos la primera serie:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
101
1
2
102
3
103
4
104
5
105
¿No se supone que la primera cola se ejecuta en serie? Lo fue (y también lo fue el segundo). Cualquier otra cosa que haya sucedido en el fondo no es motivo de preocupación para la cola. Le dijimos a la cola de serie que se ejecutara en serie y lo hizo ... pero solo le dimos una tarea. Ahora démosle dos tareas:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
Y este es el ejemplo más básico (y único posible) de serialización: dos tareas que se ejecutan en serie (una tras otra) en segundo plano (en el hilo principal) en la misma cola. Pero si los hicimos dos colas en serie separadas (porque en el ejemplo anterior son la misma cola), su salida se vuelve a mezclar:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue2.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
3
103
4
104
5
105
Y esto es lo que quise decir cuando dije que todas las colas son concurrentes entre sí. Se trata de dos colas en serie que ejecutan sus tareas al mismo tiempo (porque son colas independientes). Una cola no conoce ni se preocupa por otras colas. Ahora volvamos a dos colas en serie (de la misma cola) y agreguemos una tercera cola, una concurrente:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 1000)
}
}
1
2
3
4
5
101
102
103
104
105
1001
1002
1003
1004
1005
Eso es algo inesperado, ¿por qué la cola concurrente esperó a que las colas seriales terminaran antes de ejecutarse? Eso no es concurrencia. Su patio de recreo puede mostrar un resultado diferente, pero el mío mostró esto. Y mostró esto porque la prioridad de mi cola concurrente no era lo suficientemente alta como para que GCD ejecutara su tarea antes. Entonces, si mantengo todo igual pero cambio la QoS de la cola global (su calidad de servicio, que es simplemente el nivel de prioridad de la cola) let concurrentQueue = DispatchQueue.global(qos: .userInteractive)
, entonces el resultado es el esperado:
1
1001
1002
1003
2
1004
1005
3
4
5
101
102
103
104
105
Las dos colas en serie ejecutaron sus tareas en serie (como se esperaba) y la cola concurrente ejecutó su tarea más rápido porque se le dio un nivel de prioridad alto (una QoS alta o calidad de servicio).
Dos colas simultáneas, como en nuestro primer ejemplo de impresión, muestran una impresión desordenada (como se esperaba). Para que se impriman de forma ordenada en serie, tendríamos que hacer que ambos tengan la misma cola de serie (también la misma instancia de esa cola, no solo la misma etiqueta) . Luego, cada tarea se ejecuta en serie con respecto a la otra. Sin embargo, otra forma de hacer que se impriman en serie es mantenerlos simultáneamente, pero cambiar su método de envío:
concurrentQueue.sync {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
Recuerde, el envío de sincronización solo significa que el subproceso que realiza la llamada espera hasta que se complete la tarea en la cola antes de continuar. La advertencia aquí, obviamente, es que el hilo de llamada se congela hasta que se completa la primera tarea, que puede ser o no la forma en que desea que funcione la interfaz de usuario.
Y es por esta razón que no podemos hacer lo siguiente:
DispatchQueue.main.sync { ... }
Ésta es la única combinación posible de colas y métodos de despacho que no podemos realizar: despacho sincrónico en la cola principal. Y eso es porque le pedimos a la cola principal que se congele hasta que ejecutemos la tarea dentro de las llaves ... que enviamos a la cola principal, que simplemente congelamos. Esto se llama punto muerto. Para verlo en acción en un patio de recreo:
DispatchQueue.main.sync { // stop the main queue and wait for the following to finish
print("hello world") // this will never execute on the main queue because we just stopped it
}
// deadlock
Una última cosa a mencionar son los recursos. Cuando le asignamos una tarea a una cola, GCD encuentra una cola disponible en su grupo administrado internamente. En cuanto a la redacción de esta respuesta, hay 64 colas disponibles por qos. Puede parecer mucho, pero se pueden consumir rápidamente, especialmente en bibliotecas de terceros, en particular en los marcos de bases de datos. Por esta razón, Apple tiene recomendaciones sobre la gestión de colas (mencionadas en los enlaces a continuación); uno de ellos:
En lugar de crear colas simultáneas privadas, envíe tareas a una de las colas de despacho simultáneas globales. Para tareas en serie, establezca el destino de su cola en serie en una de las colas simultáneas globales.
De esa manera, puede mantener el comportamiento serializado de la cola mientras minimiza el número de colas separadas que crean subprocesos.
Para hacer esto, en lugar de crearlos como lo hicimos antes (que todavía puedes), Apple recomienda crear colas en serie como esta:
let serialQueue = DispatchQueue(label: "serialQueue", qos: .default, attributes: [], autoreleaseFrequency: .inherit, target: .global(qos: .default))
Para leer más, recomiendo lo siguiente:
https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091-CH1-SW1
https://developer.apple.com/documentation/dispatch/dispatchqueue