Grand Central Dispatch en Swift

GRAND CENTRAL DISPATCH (GCD) en Swift en Español

Hoy te hablamos de threads y concurrencia en una aplicación. Un buen uso de los threads nos ayuda a tener una app más rápida, sin menos bloqueos y por lo tanto una experiencia de usuario mejor.

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇

Cuando programamos una app tenemos que tener en cuenta que podemos beneficiarnos de tener múltiples threads (hilos) corriendo al mismo tiempo. A esto lo llamamos concurrencia.
Para ello usamos la clase DispatchQueue que está construida por encima Grand Central Dispatch (GCD).

Threads y Concurrencia

Te voy a explicar un caso de uso muy común, imagina que lanzas una petición a tu backend para obtener un listado de usuarios, esta operación debe ser asíncrona y lanzarse en un thread paralelo para que el usuario pueda seguir navegando en la aplicación sin problemas mientras se están obteniendo los datos (hacer scroll, dar a favorito en un profile, navegar por la app, etc). Si lo hicieramos en el thread principal la pantalla se congelaría y tendríamos que esperar a recibir todos los datos de backend para poder hacer otras acciones.

Hay una única norma, para refrescar la UI siempre tiene que ser en el main thread (el hilo principal de la aplicación). Ningún thread en background bajo ningún concepto debe actualizar la UI (pueden originarse bugs). Como por ejemplo, hacer un reload data de un UITableView o UICollectionView.

GCD es una capa abstracción que está construida por encima de la capa de los threads y nos sirve para realizar operaciones tanto asíncronas como síncronas. Como hemos mencionado en el ejemplo anterior (llamada a backend) podemos lanzar operaciones costosas en threads que no son el main thread.

Colas

GCD trabaja en colas para poder realizar las operaciones. Lanzamos operaciones/tareas que se ejecutaran en el mismo orden de entrada (FIFO, primero en entrar, primero en salir).

Hay colas que pueden hacer el trabajo en serie, o concurrente. Vamos a ver una imagen de las dos para ver las diferencias.

Como ves, las colas concurrentes tienen la ventaja de trabajar en paralelo y reducir así el tiempo en finalizar todas las tareas. Las colas en serie no empiezan una tarea hasta que no se ha acabado la tarea anterior.

En GCD añadimos tareas a las colas, y es él mismo el que decide en qué threads se ejecutan las operaciones. Las colas son thread-safe esto signigica que podemos acceder a la colas desde diferentes threads de forma simultánea. Imagínate que estás en una cola realizando una operación en background y quieres pasar al main thread para hacer un reload data (este método nos sirve para refrescar nueva información) de tu UITableView o UICollectionView, no habría ningún problema.

Es decir, decimos a GCD que queremos lanzar una operación en un thread en background y ahí nosotros llamaremos a nuestro backend. GCD se encargará de buscar ese thread.

Vamos a ver un ejemplo de cola en serie y cola concurrente

Ejemplo cola en serie en Swift

let serialQueue = DispatchQueue.init(label: "swiftbeta.serial.queue")

serialQueue.async {
    print("Started task 1...")
    print("Finished task 1...")
}

serialQueue.async {
    print("Started task 2...")
    print("Finished task 2...")
}

serialQueue.async {
    print("Started task 3...")
    print("Finished task 3...")
}

/* El resultado es:
Started task 1...
Finished task 1...
Started task 2...
Finished task 2...
Started task 3...
Finished task 3...
*/

Como ves, hasta que no acaba la tarea 1 no empieza la tarea 2. Y hasta que no acaba la tarea 2 no empieza la 3.

Ejemplo cola concurrente en Swift

let concurrentQueue = DispatchQueue.init(label: "swiftbeta.concurrent.queue", attributes: .concurrent)

concurrentQueue.async {
    print("Started task 1...")
    print("Finished task 1...")
}

concurrentQueue.async {
    print("Started task 2...")
    print("Finished task 2...")
}

concurrentQueue.async {
    print("Started task 3...")
    print("Finished task 3...")
}

/* El resultado es:
Started task 1...
Started task 3...
Started task 2...
Finished task 1...
Finished task 3...
Finished task 2...
*/

En este caso empiezan las 3 tareas a la vez.


Tipos de colas

GCD nos ofrece 3 tipos de colas

  • Main queue: es el thread principal de la app, podemos acceder a él desde cualquier punto de la app porque es accesible globalmente.
DispatchQueue.main.async {
    print("Reload collectionView")
}
  • Global queues: son colas que se comparten por todo el sistema. Tenemos distintas prioridades high, default, low y brackground. Cuando enviamos tareas a las global queues especificamos un Quality of Service (QoS), que determina esas prioridades que acabamos de mencionar: userInteractive, userInitiated, utility y background.

Global Queues

Vamos a ver cada una de las prioridades de las global queues

userInteractive
Se ejecuta en el main thread (hilo principal).
Lo usamos para hacer updates en la UI o para ejecutar lógicas que no sean muy costosas.

userInitiated
El user ha invocado una operación desde UI y debe resolverse de forma asíncrona.
La prioridad es High.

utility
Para tareas asíncroncas que necesitan tiempo para finalizar, por ejemplo las típicas al user le ponemos un spinner.
La prioridad es Low.

background
Son tareas que se ejecutan y el user no es consciente (no requiere interacción del usuario) como por ejemplo prefetching, mantenimiento y otras tareas en las que el tiempo no es importante.
La prioridad es la más baja de todas.

// La prioridad más baja
DispatchQueue.global(qos: .background).async {
    print("Background")
}

// Prioridad por defecto
DispatchQueue.global(qos: .userInteractive).async {
    print("UserInteractive")
}

// La prioridad más alta
DispatchQueue.global(qos: .userInitiated).async {
    print("UserInitiated")
}

// Prioridad baja
DispatchQueue.global(qos: .utility).async {
    print("Utility")
}

/* El resultado es:
UserInitiated
UserInteractive
Utility
Background
*/
  • Custom queues: Son colas que creamos nosotros (pueden ser en serie o concurrentes). El anterior ejemplo de colas en serie y concurrente sería un ejemplo de custom queues.
let concurrentQueue = DispatchQueue(label: "swiftbeta.concurrent.queue", attributes: .concurrent)

concurrentQueue.async {
    print("Started task 1...")
    print("Finished task 1...")
}

Síncrono vs Asíncrono

En GCD puedes lanzar colas de forma síncrona o asíncrona. Depende de como lo quieras implementar. Un ejemplo:

// Usa async para ejecutar ese bloque de código de forma asíncrona
DispatchQueue.global(qos: .default).async {
    // Do something
}

// Usa sync para ejecutar ese bloque de código de forma síncrona
DispatchQueue.global(qos: .default).sync {
    // Do something
}

Delay de una tarea

Uno de los métodos que tenemos con DispatchQueue es el de poder retrasar la ejecución de una tarea. Es tan simple como usar el método asyncAfter y especificar el tiempo en segundos, vamos a ver un ejemplo:

DispatchQueue.main.async {
    print("Message")
}

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    print("Message printed after 2 seconds")
}

DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
    print("Message printed after 4 seconds")
}

/* El resultado es
Message
Message printed after 2 seconds
Message printed after 4 seconds
*/

Como ves se ve por consola el primer mensaje nada más compilar, y al cabo de 2 y 4 segundos aparecen los siguientes mensajes.

Los threads es algo básico que debes saber para crear una aplicaicón pero antes de crear un hilo piensa si realmente lo necesitas. Abusar de ellos puede ser un problema cuando tu app escala, aparte de que tener mucho hilos tiene su coste.

Hasta aquí el post de hoy, gracias por leernos! 🤓
Si tienes preguntas no dudes en contactar con nosotros a través de Twitter