jueves, 20 de marzo de 2014

Programación concurrente iOS


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:




No hay comentarios:

Publicar un comentario