Bloques y Multitarea con Objective C, Cocoa y Grand Central Dispatch (GCD)

Autor: | Última modificación: 26 de octubre de 2022 | Tiempo de Lectura: 4 minutos
Temas en este post:

Grand Central Dispatch GCD y uso de bloques para multitarea

Esta aplicación con Grand Central Dispatch GCD quizás sea la más común e importante de los bloques en Cocoa y Cocoa Touch.

Grand Central Dispatch

Grand Central Dispatch GCD es una API C para multitarea usando hebras. De cara al usuario es muy sencilla: simplemente pones bloques en una cola que van siendo ejecutados en otra hebra. Las colas más comunes son las que ejecutan los bloques en serie (uno detrás del otro), como una cola verdadera. También las hay que ejecutan todos los bloques en paralelo, aunque son tema para otro artículo.

Aunque las CPU de los dispositivos iOS no son (a fecha de hoy, Abril de 2011) multi-nucleo, el uso de hebras tiene muchas ventajas.

Cuando una cola s queda bloqueada, las demás siguen funcionando sin problemas. Por lo tanto, toda tarea que consuma mucha CPU o que pueda bloquearse (conexiones de red) debería de ser ejecutada en otra hebra. De esta forma, la cola principal (y su hebra) no se verá bloqueada y el GUI seguirá funcionando sin retrasos ni bloqueos.

Principales funciones de la API

Crear y liberar colas

  1. dispatch_queue_t dispatch_queue_create(const char *label, NULL);     //crear nueva cola
  2. void dispatch_release(dispatch_queue_t);                             //liberar dicha cola

Una cola no será destruida (dealloc) mientras haya bloques esperando ser ejecutados. Por lo tanto, llamar a dispatch_release() no causará la destrucción inmediata de la cola.

El primer parámetro de dispatch_queue_create es el nombre que se le dará a la cola. Esto es útil para la depuración. ¡Ojo que es un const char * y no un NSString *!

El segundo parámetro es de opciones para la creación de la cola, pero de momento podemos dejarlo siempre como NULL. Ojo, NULL, no nil.

Poner bloques en la cola

typedef void (^dispatch_block_t) (void);

void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

Obtener la cola actual y la principal

dispatch_queue_t dispatch_get_current_queue(); dispatch_queue_t dispatch_get_main_queue();

Si vamos a crear un bloque que va a ejecutar código de UIKit o CoreData, hay que asegurarse de ejecutarlo en la cola y hebra principal, ya que ninguna de las frameworks es segura para multitarea.

Ejemplo de bloques y Grand Central Dispatch

Código que prepara una jerarquía de vistas para enseñar una imagen bajada de la red. Consiste en una UIScrollView, UIImageView y una UIActivityIndicatorView. Esta última no es una subvista de la UIScrollView, para que esté siempre en el centro indicando que «estamos en ello».

El código que baja la imagen en paralelo es EXACTAMENTE igual al que la baja en serie. La única modificación que tenemos que hacer es rodearlo con un dispatch_async(xxx, ^{ …. }).

Ojo, que como parte del código usa UIKit, éste a su vez se ejecuta dentro de otro bloque (un bloque dentro de otro) que se asigna a la cola principal.

  1. (void) viewWillAppear:(BOOL)animated{
  2. [superviewWillAppear:animated];
  3. NSString *const kURLString = @«http://www.destination360.com/north-america/us/idaho/images/s/idaho-sawtooth-mountains.jpg»;
  4. self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
  5. self.scrollView.backgroundColor = [UIColor blackColor];
  6. self.imageView = [[UIImageView alloc] initWithFrame:self.scrollView.bounds];
  7. [self.scrollView addSubview:self.imageView];
  8. [self.view addSubview:self.scrollView];
  9. self.activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
  10. self.activityView.hidesWhenStopped = YES;
  11. [self.activityView startAnimating];
  12. [self.view addSubview:self.activityView];
  13. self.activityView.center = self.scrollView.center;
  14. dispatch_queue_t downloader = dispatch_queue_create(«downloader», NULL);
  15. dispatch_async(downloader, ^{
  16. // Potentially blocking stuff goes in another thread
  17. NSError *err = nil; // if declared outside, it would be a constant pointer.
  18. NSData *imgBlob = [[[NSDataalloc]
  19. initWithContentsOfURL:[NSURLURLWithString: kURLString]
  20. options:NSDataReadingMapped
  21. error: &err] autorelease];
  22. dispatch_async(dispatch_get_main_queue(), ^{
  23. if (!err) {
  24. // UI stuff cannot be done outside the Main Thread/Queue
  25. UIImage *img = [UIImageimageWithData:imgBlob];
  26. self.imageView.image = img;
  27. self.imageView.frame = CGRectMake(0, 0, img.size.width, img.size.height);
  28. self.scrollView.contentSize = img.size;
  29. [self.activityViewstopAnimating];
  30. }else{
  31. NSLog(@«%@»,[err localizedDescription]);
  32. }
  33. [self.activityViewstopAnimating];
  34. });
  35. });
  36. dispatch_release(downloader);
  37. }

Adaptar tu API al asincronismo

Si tenemos un objeto con una propiedad image con un getter

-(UIImage *) image;

y queremos que el obtener dicha imagen de la red sea en paralelo, tendremos que pensar en cómo modificar nuestro getter. Tal como está no funcionará, ya que retornará de inmediato, ¡antes de que se haya obtenido los datos de la red!

Podríamos crear un método que inicie la descarga y una notificación, o un método de delegado, que nos avise cuando ha terminado. Sin embargo, los bloques nos dan una alternativa infinitamente más sencilla, al usarlos como gestores de finalización.

Creamos un método que acepta un bloque que se ejecutará al terminar:

-(void)processImageDataWithBlock: (void (^)(NSData* imageData)) imageProcessorBlock;

o bien, incluyendo también un gestor de error:

-(void)processImageDataWithBlock: (void (^)(NSData* imageData)) imageProcessorBlock
       andErrorWithBlock:(void (^) (void)) errorBlock;

Contexto en el que se debe ejecutar un gestor de error o finalización

¡OJO! Al implementar dicho método, hay que ejecutar el código de descarga en una hebra a parte, pero ¡los gestores han ser ejecutados el la hebra actual! Como no sabemos si incluyen código UIKit o CoreData (y es muy probable que sea así), podríamos causar graves problemas (deadlocks, race conditions) si lo ejecutamos en una hebra que no sea la actual.

Para obtener la cola actual se usa la función:

dispatch_queue_t dispatch_get_current_queue();

Y para implementar el método, se debe usar la técnica de un bloque dentro del otro ya citada.

Fernando Rodríguez

Sígueme en twitter.
Cursos de desarrollo iPhone