La concurrencia es vital. Sin ella, nuestros ordenadores y teléfonos inteligentes no proporcionarían la experiencia de usuario perfecta en la que hemos llegado a confiar. Los ordenadores y sistemas operativos de hoy en día pueden iniciar, ejecutar y terminar múltiples tareas en el mismo período de tiempo. Esto nos permite interactuar con la interfaz de usuario mientras la aplicación realiza tareas en segundo plano como la creación de redes, el archivado de E/S, las consultas a la base de datos u otras operaciones de larga duración.
Si bien los usuarios se benefician indudablemente de la programación simultánea, la aplicación real del concepto puede ser un desafío. En la Edad de Piedra de la Informática (hace unos cuarenta años), los ordenadores personales no eran capaces de ejecutar múltiples tareas a la vez. Luego vino la evolución de las arquitecturas de CPU; los ordenadores empezaron a soportar la ejecución de múltiples procesos o hilos. Esto era genial, pero los sistemas operativos y las aplicaciones seguían estando rezagados.
Pero como todo lo que parece demasiado bueno para ser verdad, la concurrencia viene con varios peligros. Como desarrollador, querrás estar atento a una serie de problemas potenciales (piensa en la «lata de gusanos»: una vez que la abras, prepárate para los problemas).
Multihilo
Por supuesto, los lenguajes de programación no facilitaron la implementación del multihilo. La mayoría de los lenguajes simplemente proporcionaban acceso a las APIs nativas de subprocesos de bajo nivel y a las construcciones del sistema operativo subyacente, y cada sistema operativo utilizaba una API de subprocesos diferente.
Pronto aparecieron soluciones estandarizadas, incluyendo los hilos POSIX. PThreads es una biblioteca C que expone un conjunto de funciones que pueden ser implementadas por los proveedores de sistemas operativos. Este enfoque permite utilizar la misma interfaz a través de múltiples plataformas. PThreads es soportado por plataformas UNIX incluyendo iOS, macOS y Linux.
Los charcos de hilos son otro concepto destinado a simplificar un enhebrado abstracto. En el núcleo de este patrón se encuentra la idea de tener un número de hilos pre-creados y ociosos que están listos para ser utilizados. Cada vez que hay una nueva tarea a ser ejecutada, el hilo se despierta, realiza la tarea y luego regresa a la inactividad.
Entonces, ¿por qué querrías crear un montón de hilos para mantenerlos alrededor? En una palabra: rendimiento.
En lugar de crear un nuevo hilo cada vez que se va a ejecutar una tarea (y luego destruirlo cuando la tarea termine), los hilos disponibles se toman del fondo de hilos. La creación y destrucción de hilos es un proceso costoso, por lo que el patrón de reserva de hilos ofrece considerables ganancias de rendimiento. Dejar que la biblioteca o el sistema operativo gestione los hilos significa que tienes menos de qué preocuparte (lectura: menos líneas de código que escribir). Además, la biblioteca puede optimizar la gestión de los hilos detrás de las escenas.
Concurrencia y paralelismo
Las tareas concurrentes pueden ser ejecutadas a través del paralelismo. El paralelismo se confunde a menudo con la concurrencia, y aunque estos conceptos están relacionados, es importante saber que son cosas diferentes. El paralelismo sólo puede lograrse en dispositivos de varios núcleos; mientras que un núcleo ejecuta una tarea, el otro núcleo puede ejecutar la otra tarea simultáneamente, así:
El paralelismo, sin embargo, no es necesario para la concurrencia. Con los dispositivos de núcleo único, la concurrencia se logra mediante el cambio de contexto: el núcleo ejecuta una tarea durante algún tiempo, luego cambia a la otra tarea o proceso, la ejecuta, luego vuelve a cambiar a la tarea anterior, y así sucesivamente, hasta que la tarea se completa.
La concurrencia a través del cambio de contexto no arruina la ilusión porque el cambio se produce rápidamente. Con el verdadero paralelismo, la ejecución de tareas concurrentes es más rápida. Además, un interruptor de contexto requiere almacenar y restaurar el estado de ejecución cuando se cambia de hilo, lo que significa una sobrecarga adicional.
Grand Central Dispatch (GCD)
Grand Central Dispatch (GCD) es el marco de Apple y se basa en el patrón de hilo de la piscina. GCD estuvo disponible por primera vez en 2009 con MAC OS X 10.6/Snow Leopard e iOS 4. En el núcleo de GCD está la idea de los elementos de trabajo, que pueden ser enviados a una cola; la cola oculta todas las tareas relacionadas con la gestión de hilos. Puedes configurar la cola, pero no interactuarás directamente con un hilo. Este modelo simplifica la creación y la ejecución de tareas asincrónicas o síncronas.
GCD abstrae la noción de hilos, y expone las colas de envío para manejar los elementos de trabajo (los elementos de trabajo son bloques de código que se quieren ejecutar). Estas tareas se asignan a una cola de envío, que las procesa en un orden de «primero en entrar, primero en salir» (FIFO).
Colas de GCD en serie y concurrentes
Hay dos tipos de colas en el GCD. Estas se conocen como seriales y concurrentes.
Primero, hablemos de las colas en serie. Si envías elementos de trabajo a una cola en serie, se ejecutarán uno tras otro en el orden en que fueron añadidos. Como es una cola en serie, no hay concurrencia de ningún tipo. Una cola de envío en serie siempre ejecuta un elemento de trabajo a la vez.
Ahora, echemos un vistazo rápido a las colas de envío simultáneo. Los elementos de trabajo sometidos a una cola de envío simultáneo comenzarán en el orden de su adición a la cola. El número de tareas que se ejecutarán simultáneamente, y el tiempo que toma ejecutar la siguiente tarea, es controlado por la cola. Es importante señalar que no podemos influir en este comportamiento.
Además, está totalmente oculto si la concurrencia se logra a través del paralelismo, o a través del cambio de contexto.
El GCD nos oculta estos detalles, y el resultado es una API muy simple, que fue refinada en Swift 3.0.
El siguiente fragmento ilustra la simplicidad de la ejecución simultánea de los elementos de trabajo utilizando una cola de despacho simultánea GCD:
// crear la cola concurrente asyncQueue = DispatchQueue(label: "asyncQueue", atributos: .concurrent)// realizar la tarea asincrónicamenteasyncQueue.async { // realizar alguna tarea de larga duración aquí}
¿No es elegante? Crear la cola concurrente es pan comido. Primero, pasamos un identificador único, y luego especificamos la naturaleza concurrente de esta cola estableciendo el argumento de los atributos en .concurrent. (Nota: Si se omite el argumento de atributos, la cola será serial por defecto).
GCD en Swift 3
El GCD fue completamente renovado en Swift 3. En versiones anteriores de Swift, GCD expuso un conjunto de funciones C; Swift 3 se alejó de la API C y todas las funciones C globales han desaparecido. Lo que tenemos ahora son tipos de Swift dedicados, funciones de miembros, y propiedades, todo lo cual proporciona una interfaz Swift más natural. Puede leer más sobre la propuesta aquí.
Ahora, usar la cola es aún más fácil. Simplemente llama al método de colas async(), y pasa el bloque de código que deseas que se ejecute en segundo plano. Normalmente, querrás ejecutar código asincrónico que consume mucho tiempo para evitar bloquear el hilo principal de la interfaz de usuario.
Puede parecer que hemos cubierto bastante aquí, pero apenas hemos arañado la superficie. Por encima de todo, recuerden que Grand Central Dispatch facilita el apoyo a la concurrencia en nuestras aplicaciones. En lugar de manejar hilos, cerraduras y semáforos, ahora puedes concentrarte en la verdadera tarea de implementación. Ejecutar tareas informáticas costosas en segundo plano probablemente nunca ha sido tan fácil. Una vez que entienda completamente el concepto de grupos de hilos, colas de envío y elementos de trabajo, agregar la concurrencia a sus aplicaciones será casi tan fácil como cualquier otra tarea de programación típica.
Aprenda más sobre los patrones de diseño creativo y la concurrencia en Swift 3 aquí.
COMPARTIR: