iPhone et l’univers parallèle

Publié le Mis à jour le Par

Impossible de créer des threads sur iPhone ! C’est ce qu’on peut entendre des personnes qui ne connaissent pas techniquement la plateforme et s’arrêtent au fait qu’une seule application tiers peut être active à un instant donné.
En réalité, il est possible (et souhaitable !) de paralléliser les traitements (surtout pour les requêtes serveurs) fait par votre application mobile. Cette recommandation vaut pour toutes les plateformes.

Mais alors comment faire sur iPhone ? Plusieurs solutions existent !

Basique

L’Objective-C permet de créer des threads en s’appuyant sur POSIX ; rien sur iPhone ne l’interdit. Leur gestion est complexe et laborieuse. Nous ne l’aborderons pas car :
– Le SDK fournit d’autres solutions plus élégantes ;
– Les verrous et les deadlocks sont synonymes de malheur sur plusieurs générations ;
– Un billet ne suffirait pas…

Message en arrière plan

La façon la plus simple est d’envoyer le message performSelectorInBackground à votre objet. Ce message met votre application en mode multithreading, si ce n’était pas déjà le cas, et lance un nouveau thread appelant la méthode. Dans cette configuration, il est à la charge de la méthode appelée de positionner l’environnement du thread. C’est à dire par exemple de créer un pool d’autorelease et de le libérer à la fin de la méthode.

Cette méthode est très pratique pour les méthodes privées ou des traitements simples réalisés dans un environnement clos. Si un processus plus long ou plus complexe est à paralléliser, il vaut mieux passer par d’autres méthodes.

- (void) viewDidLoad <em>[self performSelectorInBackground:@selector(loadXML) withObject:nil] ;
</em>

- (void) loadXML <em>// Code chargeant un xml sur un serveur distant
</em>


Ce code va simplement appeler la méthode de chargement d’un xml disponible sur un serveur dans un thread à part, créer automatiquement.

Grand Central Dispatch

Apple a ajouté à Snow Leopard un moyen pour les développeurs de réaliser simplement des traitements concurrents : Grand Central Dispatch. Il fournit une suite de fonctions C gérant de manière transparente les threads. Depuis iOS 4, il est également disponible aux développeurs iPhone et iPad.

L’utilisation de GCD peut se faire par le biais de blocks. Les blocks sont des closures dans le monde Objective-C. Ils sont également employés dans d’autres contextes comme les animations depuis la dernière API de iOS.

imageView.frame = CGRectMake(0, 420, 320, 46) ;
[self.view addSubview:imageView] ;

void (^myAnimationBlock)(void) = ^(void) <em>imageView.frame = CGRectMake(0, 366, 320, 46) ;
</em> ;
[UIView animateWithDuration:0.5 animations:myAnimationBlock] ;


Un block se définit un peu comme un pointeur de fonction.

Pour décortiquer un peu la déclaration d’un block :

NSString* (^multiply)(int num1, int num2) = ^(int num1, int num2) <em>int value = num1*num2 ;
return [NSString stringWithFormat:@"%i * %i = %i", num1, num2, value] ;
</em> ;

Dans cet exemple :
– NSString* : définit le type de la valeur de retour
– ^multiply : définit le nom de la variable de type block
– (int num1, int num2) : définit les paramètres pris par le block

Mais revenons au vif du sujet. Il est possible d’appeler directement GCD via des fonctions comme dispacth_async. Cette dernière vous demande le traitement à paralléliser, via un block, et la file d’attente. Celle-ci sert à GCD pour dépiler au fur et à mesure les traitements.

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^(void) <em>[self loadXMLSync] ;
dispatch_async(dispatch_get_main_queue(), ^(void) {
[self updateUI] ;
</em>) ;
}) ;


Comme précédemment, ce code demande le chargement asynchrone d’un XML depuis un block exécuté sur un autre thread. Une fois que la méthode loadXMLSync rend la main, on met à jour l’interface utilisateur, toujours de façon asynchrone, mais sur le thread principal.

Une des gestions des files disponible dans GCD alloue un nombre de thread par file d’attente et les crée tous en même temps. Ils ne sont jamais libérés ce qui permet un gain de performance en évitant les allés-retours : création, libération, création, … En fonction de leur disponibilité, GCD leur affecte les traitements placés dans sa file d’attente.

D’autres fonctions sont disponibles avec GCD telles que la gestion de sémaphores et d’événements système bas niveau.

NSOperationQueue

Il y a enfin la classe NSOperation. Cette solution n’est pas nouvelle à iOS 4, mais utilise GCD depuis la dernière version de l’OS. Le principe de la gestion d’une file d’attente sur les traitements est la même que celle de GCD. Apple offre un niveau d’abstraction au dessus de GCD : vous manipulez des objets de l’API Foundation au lieu de faire appel à des fonctions en C. La gestion des dépendances entre traitements est également facilitée.

NSOperationQueue* queue = [[NSOperationQueue alloc] init] ;
[queue addOperationWithBlock:^<em>[self loadXMLSync] ;
[[NSOperationQueue mainQueue] addOperationWithBlock :^{
[self updateUI] ;
</em>] ;
}] ;


Ce code fait exactement la même chose que le précédent.

A noter

Le framework UIKit (la couche graphique de CocoaTouch) n’est pas thread-safe. Il est donc recommandé de ne pas modifier l’IHM dans un thread différent du thread principal.

N’oubliez pas que vous avez des objets immutable dans Foundation (NSString, NSArray, …). Il est utile de réfléchir à leur utilisation plutôt que leur alternative mutable lorsqu’on intègre des traitements concurrents dans son application. A ce propos, je vous rappelle une petite différence lorsque vous voulez accéder à une propriété de votre classe :
myProperty : vous accédez directement à la propriété sans filet
self.myProperty : vous accédez à la propriété au travers d’accesseurs que vous pouvez sécuriser

Enfin il faut porter une attention toute particulière aux notifications. Par défaut, les notifications sont envoyées sur le thread qui a été utilisé pour les émettre. Ce qui veut dire qu’un objet qui s’est abonné à une notification n’est pas nécessairement sur le thread où cette dernière à été émise !