1.-
Introducción
El
iPhone 4 fue el primer dispositivo móvil de la marca Apple en
incluir un procesador multi-core. Esta característica permite
ejecutar varias tareas de forma concurrente de forma “real”. Esta
ultima reseña es importante ya que en versiones anteriores al
disponer de un solo núcleo la ejecución simultanea de tareas debía
de “simularse” ().
Por
tanto en la versión iOS 4 Apple se introdujeron entre otras cosas
dos funcionalidad muy importantes al SDK : los bloques y el GCD (Gran
Central Dispatch), para
aprovechar al máximo esta nueva características en los dispositivos
y facilitar la programación concurrente.
2.-
Conceptos previos
En
iOS todo nuestro programa se ejecutará sobre una misma hebra llamada
hebra principal en la cual por “norma general” se debe ejecutar
todo lo referente a la interfaz gráfica. Esta hebra además es la
encargada de recibir los eventos generados por el usuario o por otras
aplicaciones.
2.1.-
Los bloques (Block):
Los
bloques son fragmentos de
código empaquetados y
pueden ser visto como
funciones en el sentido de que encapsulan un conjunto de sentencias
pero tienen dos grandes diferencias con respecto a las funciones:
-
Pueden ser pasados como parámetros a funciones o métodos.
-
Los bloques capturan el entorno semántico en el que fueron
definidos. Es decir tienen acceso a variables que están definidas
fuera del bloque:
- En el caso de las funciones las variables a las que podrá acceder serán a las declaradas de forma local y a las pasadas como parámetros.
- En el caso de los bloques además de acceder a las variables locales y los parámetros, tambien puede acceder a las variables definidas en el mismo ámbito donde se definió el bloque. Ejemplo:
int multiplicar = 10; int(^aumentar)(int) = ^ (int n){return n*multiplicador;}; int valor = aumentar(3) // valor = 30;
Si se quisiera modificar el valor de la variable “multiplicar” se debería de declarar con el prefijo __block delante.
La
forma de declarar un bloque (o un literal de bloque) es la
siguiente:
^ tipoRetorno (parametros){ cuerpo bloque return valor; }
Si
quisiéramos almacenar este bloque en una variable para después
poder usarlo se deberá almacenar en un puntero a bloque :
TipoRetorno
(^nombrePunteroBloque) (parametros);
A continuación se muestra un
ejemplo:
int (^multiplica) (in, int) = ^ int (int a, int b){ return a*b; } int c = multiplica(a*b);
Como vemos en el ejemplo a la
izquierda de la asignación definimos el puntero a bloque
(multiplica) y a la derecha de la asignación definimos el bloque con
el código a ejecutar.
3.-
Gran Central Dispatch
GCD
es una api creada por Apple para el manejo y uso de procesos
concurrentes de tal forma que podamos lanzar varias hebras de forma
concurrente sin tener que preocuparnos de los detalles de más bajo
nivel.
Haciendo uso de los bloques
explicados en el apartado anterior para ejecutar varias tareas de
forma paralela y fuera de la hebra principal basta con crear una cola
en la cual se insertan dichas tareas. Estas tareas serán ejecutadas
en diferentes hebras y estas hebras será gestionadas por la cola.
GCD
nos proporciona diferentes colas para que se ajusten mejor a nuestras
necesidades. En todas ellas
domina o se gestionan por el orden FIFO (“primera
tarea en entrar, primera
en salir”):
-
Serial dispatch queue (cola en serie): las tareas se ejecutan una
detrás de otra, es decir
solo se está ejecutando una tarea en un ciclo.
Son las más usadas y son muy útiles
cuando la ejecución de una tarea depende del resultado de otra o
se quiere garantizar el acceso concurrente a un recurso.
-
Concurrent dispatch queue (cola concurrente): por el contrario la
cola concurrente ejecuta
tantas tareas como pueda (dependiendo
de estado del sistema) de
forma concurrente sin necesidad de esperar a la finalización de
otra. A partir de la versión
iOS5 podemos crear nuestras propias colas concurrentes o usar las
colas creadas por ellos o ya predefinidas que son:
dispath_queue_priority_high DISPATH_QUEUE_PRIORITY_HIGH,
DISPATH_QUEUE_PRIORITY_LOW, DISPATH_QUEUE_PRIORITY_DEFAULT y
DISPATH_QUEUE_PRIORITY_BACKGROUND.
-
Main queue (cola principal):
se trata de una cola disponible de forma global y es la encargada de
ejecutar las tareas asignadas a la hebra principal.
Después
de toda esta explicación lo mejor será ver un sencillo ejemplo que
ilustra muy bien todo lo explicado hasta el momento:
dispatch_queue_t colaEnSerie = dispatch_queue_create("miColaEnSerie", NULL);
dispatch_async(colaEnSerie, ^{
NSLog(@"Entra en la tarea 1");
[NSThread sleepForTimeInterval:3.0];
NSLog(@"Sale de la tarea 1");
});
dispatch_async(colaEnSerie, ^{
NSLog(@"Entra en la tarea 2");
[NSThread sleepForTimeInterval:1.0];
NSLog(@"Sale de la tarea 2");
});
En este ejemplo lo que hacemos
es crear una cola en serie y asignarle dos tareas (cada tarea
corresponde aun “bloque”) : una para que la hebra duerma durante
3s y la otra para que la hebra duerma 2s. El resultado obtenido es el
siguiente:
2012-03-23 13:51:54.223 GDC Test[1587:12e03] Entra en la tarea 1
2012-03-23 13:51:57.228 GDC Test[1587:12e03] Sale de la tarea 1
2012-03-23 13:51:57.229 GDC Test[1587:12e03] Entra en la tarea 2
2012-03-23 13:51:58.231 GDC Test[1587:12e03] Sale de la tarea 2
Como se puede observar con
claridad la tarea 2 no puede ejecutarse hasta que la tarea 1 no
termina de ejecutarse y por tanto se muestra después aunque su
duración sea menor debido al tipo de cola usado.
Ahora usaremos una cola
concurrente:
dispatch_queue_t colaConcurrente = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(colaConcurrente, ^{
NSLog(@"Entra en la tarea 1");
[NSThread sleepForTimeInterval:3.0];
NSLog(@"Sale de la tarea 1");
});
dispatch_async(colaConcurrente, ^{
NSLog(@"Entra en la tarea 2");
[NSThread sleepForTimeInterval:1.0];
NSLog(@"Sale de la tarea 2");
});
Y el resultado obtenido es el
siguiente:
2012-03-23 13:54:48.497 GDC Test[1638:13003] Entra en la tarea 2
2012-03-23 13:54:48.497 GDC Test[1638:12f03] Entra en la tarea 1
2012-03-23 13:54:49.501 GDC Test[1638:13003] Sale de la tarea 2
2012-03-23 13:54:51.502 GDC Test[1638:12f03] Sale de la tarea 1
Ahora como se puede observar
ambas tareas se a ejecutado al mismo tiempo terminando la tarea 2
antes debido a que su duración es menor.
Otro
ejemplo un poco más complejo y algo más practico es el siguiente:
en el cual se pretende mostrar por la pantalla del dispositivo una
imagen descargada de internet. La forma tradicional implicaría que
la descarga de la imagen se produjese en el ámbito de la hebra
principal, siendo este proceso altamente costoso
y por tanto bloqueando la hebra principal durante su ejecución. Esto
tiene un gran error debido a que durante la ejecución del proceso de
descarga la hebra principal permanecería bloqueada y por tanto
cualquier evento generado por el usuario pasaría desapercibido. Esto
al usuario le daría la sensación de que la aplicación no responde
y se ha quedado “pillada” traduciéndose en un aumentando de
forma considerable de su frustración. Por tanto para evitar esto lo
que haremos será asignar el proceso de descarga a otra hebra
liberando así a la hebra principal de esta tediosa tarea:
- (IBAction)segundoPlano:(id)sender { // Creamos la URL para nuestra imagen NSURL *url = [NSURLURLWithString:@"http://www.kennethprimrose.co.uk/wp-content/uploads/2011/06/Scottish-landscape.jpg"]; // Obtenemos la cola de segundo plano dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); // Invocamos en modo asíncrono un bloque en la cola que hemos recuperado dispatch_async(backgroundQueue, ^{ // Obtenemos los datos de la imagen (esto tardara un poco al // tratarse de una imagen de alta resolución) NSData *dataImagen = [NSData dataWithContentsOfURL:url]; // Creamos un UIImage con los datos obtenidos UIImage *imagen = [[UIImage alloc] initWithData:dataImagen]; dispatch_async(dispatch_get_main_queue(), ^{ // Asignamos al UIImageView de nuestra vista dicho UIImage self.imageView.image = imagen; }); }); }
Si nos fijamos bien una vez
descargada la imagen se delega la tarea de mostrarla por pantalla a
la hebra principal introduciendo esta tarea en la cola primaria. Esto
es debido a que no es seguro ejecutar el framework UIKIt (el
encargado de todo lo relacionado con la IU) en segundo plano y se
recomiendo que siempre se ejecuta bajo la hebra principal como se
comento previamente.
Dado
que en las colas lo que se encuentras son bloques de código y estos
pueden contener variables “compartidas”
ya sea porque son externas
al bloque (mediante el prefijo
__block delante de la
variable se puede modificar
dicha variable) o
porque han sido pasadas como parámetros
para su modificación, distintas tareas podrían modificar la misma
variable al mismo tiempo
y generar errores no deseados. Para evitar esto GCD nos proporciona,
entre otras cosas, funciones
para el uso de semáforos o acceso a Sección Crítica
de forma sincronizada:
Semáforos: dispatch_semaphore
Acceso
SC : dispatch_sync(my_queue, block)
4.-
Conclusiones
En resumen como hemos podido
observar la realización de nuestro programa de forma concurrente se
vuelve algo muy sencillo con esta actualización que nos proporciona
Apple, abstrayéndonos por completo de los problemas de bajo nivel
que implica el trabajar con hebras al ser el sistema el encargado de
la gestión de la cola de tareas. Además el esfuerzo por adaptar
nuestra aplicación a una aplicación multi-thread se verá
recompensado de cara al usuario proporcionándole una aplicación
mucho más rápida y menos propensa a “cuelgues” no deseados.
5.-
Bibliografía: