Core data et multithreading

Core data et multithreading

core data iconApparu avec la version 10.4 Tiger d’OS X et la version 3.0 d’iOS, Core Data est le framework incontournable pour le stockage de vos données persistantes et le mapping objet-entité. Si vous utilisez des fichiers XML ou une base de données relationnelles SQLite, vous vous éviterez bien des tracasseries en passant par l’ORM maison d’Apple et vous suivrez d’ailleurs ainsi leur recommendation. Via Xcode, vous allez pouvoir définir simplement votre modèle de données et générer les classes nécessaires. Vous ne verrez plus une seule requête SQL dans votre code et manipulerez directement (ou presque) des objets.

Tout cela est bien beau, mais, bien sûr, Core Data vient avec quelques limitations et une particulièrement, qui n’est pas des moindres, puisqu’en l’état vous risquerez de vous retrouvez avec une application complètement instable si celle-ci utilise du multi-threading, autrement dit si vous êtes friands des performSelectorInBackground et autre NSThread.

Développer une application mobile sans utiliser de tâche en arrière plan est quasiment chose impossible. La plupart de ces applications vont utiliser des webservices et vont donc communiquer de façon très régulière avec un serveur web. Si tous ces appels et les traitements liés sont effectués via la tâche de premier plan (c’est à dire celle qui gère entre autre les interactions et l’affichage de vos écrans) vous vous retrouverez avec une application qui freeze toutes les 5 secondes ou au mieux avec des écrans d’attente qui nous rappellent l’époque du sablier sous Windows. En termes d’expérience utilisateur, il n’y a rien de pire. Certains diront qu’il suffit de faire ces appels aux webservices de façon asynchrones (c’est à dire sans que le processus n’ait besoin d’attendre la réponse du serveur), sauf que le traitement de la réponse du serveur (le callback) se fait lui dans votre thread principal et bloque donc tout autre traitement (si si, vérifiez avec des gros paquets de données à traiter vous verrez !).

Ok, Core Data et Multithreading sont tous les deux indispensables  mais pas compatibles ??

Pas exactement…

Comprendre le problème : le NSManagedObectContext de Core Data

Le coeur du problème réside dans le fait que les entités NSManagedObjectContext de Core Data ne sont pas thread-safe. C’est à dire qu’avec plusieurs processus tournant en parallèle plusieurs actions simultanées sur un même objet Managed Object Context (MOC) fera tout bêtement planter votre application, le système d’exploitation ne sachant pas à qui donné la priorité sur l’objet, dans quel ordre effectuer les opérations demandées.

Un objet Managed Object Context (MOC) va représenter une vue du contenu de votre persistent store et va vous permettre de récupérer les objets qui y sont stockés et d’effectuer les différentes transactions et manipulations nécessaires à la logique de votre application. Un MOC sera relié à un objet NSPersistentStoreCoordinator, qui jouera le rôle d’interface entre le store et le MOC, ou a un autre MOC (qui sera alors son MOC père). Lorsque vous souhaiterez sauvegarder de façon persistante ou récupérer des données fraîches de votre store c’est au Persistent Store Coordinator (PSC) que le MOC relayera la demande (ou de MOC en MOC jusqu’au PSC).

Le PSC a deux avantages : il permet d’agréger plusieurs persistent store (votre application nécessite plusieurs bases de données, éventuellement de types différents) et surtout il est… thread safe. Et c’est ce dernier point qui va nous permettre de nous sortir des limitations de Core Data au niveau du Managed Object Context.

Core Data et multithreading : des solutions !

Il existe au moins trois façons différentes de résoudre ce problème avec Core Data. Dans la plupart des cas, une seule de ces méthodes répondra complètement à notre besoin.

Il s’agit de mettre en place un Managed Object Context différent pour chacun de vos thread. Ces managed object contexts sont eux même rattachés à un même Persistent Store Coordinator qui servira donc à gérer les accès concurrentiels et à effectuer les manipulations nécessaires sur le store ou les stores.

Comment implémenter cela simplement ?

Nous vous recommandons fortement d’utiliser les managers d’entités. Ces classes vous faciliterons le confinement en vous permettant simplement de vous assurer que toutes les manipulations effectuées à l’intérieur du manager soient effectuées sur le Managed Object Context dédié au thread en cours.

Tour d’abord, nous allons définir le Managed Object Context dans votre thread principal, celui qui gère entre autre ,les mises à jour et les interactions avec l’interface utilisateur. Ce MOC aura besoin d’être le plus à jour possible des données du store pour cela nous allons aussi mettre en place un observer qui nous avertira de toute sauvegarde des données d’un autre MOC. En effet, les différents MOC de votre application sont indépendants et ne contiennent pas forcément les mêmes données à un instant donné même après une sauvegarde dans le PSC. Pour que cela soit le cas, il nous faut forcer un merge entre les modifications apportés par l’autre MOC et notre MOC du thread principal.

Les autres méthodes managedObjectModel et persistentStoreCoordinator préparant les objets éponymes nécessaire à l’utilisation de Core Data sont conformes à ce que vous aviez auparavant, rien de spécifique à implémenter ici. Attention cependant, la méthode persistentStoreCoordinator doit être publique et accessible depuis vos thread secondaire afin d’y associer les MOC spécifiquement créés pour ces threads.

Voici, un exemple de méthode a appelé dans votre méthode ou votre bloc de code implémentant la logique de votre thread :

Dans les entity manager que vous utilisez dans vos threads pour manipuler vos objets, pensez à prévoir une méthode d’initialisation prenant en compte le MOC spécifique au thread en cours.

Voilà, c’est tout ! Plus de prise de tête avec Core Data !

Attention cependant, n’oubliez pas que le MOC mais également les Managed objects associés ne sont pas thread-safe. Si vous devez passer des Managed Object d’un thread à un autre, vous ne pourrez pas le faire sous peine d’assister à de beaux crashs de votre application. La bonne façon de le faire est de passer le NSManagedObjectID de votre objet au thread cible. Et de récupérer l’objet concerné via la méthode objectWithId du MOC du thread cible. Pensez également à utiliser la méthode performSelectorOnMainThread de votre viewController dans tous les appels à des méthodes susceptibles de mettre à jour l’UI. Toute mise à jour de l’interface utilisateur doit effectivement se faire dans le thread principale sinon votre interface n’est pas rafraîchit !

D’autres solutions ?

Il existe d’autre méthodes plus ou moins efficaces pour gérer le multithreading avec Core Data.

La première, nous n’allons pas réellement l’envisager. Elle ne tire pas partie des avantages du PSC et consiste à mettre en place des mutex (voir NSLocking) lors de l’utilisation du Managed Object Context ou de tout objet issue de ce MOC qui permettrait de bloquer les accès concurrentiels et de faire attendre les processus qui cherchent à manipuler le MOC ou tout objet affilié alors qu’ils sont déjà utilisés par un thread concurrent. Cette solution est difficile à mettre en place. En effet, la mise en place des mutex doit être faite de manière systématique qu’il s’agisse d’opérations de mise à jour, de création, ou de récupération d’entités, toutes doivent être faites en verrouillant le contexte concerné avant chacune de ces opérations et en les libérant par la suite. C’est un travail de fourmis et vous finirez par vous arracher les cheveux à chercher le petit bout de code pour lequel vous avez oublié de verrouiller le MOC. Enfin, et c’est un argument de poids, ces verrouillages seront fait à tout bout de champs bloquant vos processus parallèles qui attendraient alors de pouvoir accéder au MOC. De fait, vous perdez l’avantage du multi-threading puisque vos processus s’attendent mutuellement. Bref, nous vous déconseillons fortement de l’envisager (voir le dernier paragraphe de la documentation Apple à ce sujet)

La deuxième méthode consiste à utiliser la hiérarchisation des Managed Object Context de Core Data. Nous avons vu précédemment qu’au lieu de rattacher un MOC directement à un Persistent Store Coordinator, nous pouvions le rattacher à un autre MOC qui jouerait le rôle de père et ainsi de suite jusqu’au MOC père ultime qui serait lui rattaché au PSC. A première vue, cette méthode n’apporte pas grand chose par rapport à la complexité qu’elle rajoute. En fait, la hiérarchisation apporte une chose, qui n’est pas forcément nécessaire dans tous les cas. Le MOC père peut décider (ou pas) de prendre en compte les modifications apportés par le MOC fils. Cela peut être intéressant si l’ont doit implémenter un système qui permet d’annuler des modifications ou de gérer des brouillons. Cette fonctionnalité ne répond pas forcément au problème de multithreading et n’apporte rien de plus à la résolution de cette problématique. Il existe de nombreuses autres façons d’implémenter cette fonctionnalité supplémentaire sans avoir recours au coeur Core Data. De plus, l’utilisation de cette hiérarchisation des MOC (appelée nested MOC en anglais) semble poser de nombreux problèmes et est un peu plus compliqué à stabiliser dans les faits. Pourquoi faire compliqué quand on peut faire simple ?

Core Date et multithreading – À lire :

Concurrency with Core Data – Core Data Programming Guide in iOS Developer Library

Introduction to Core Data Programming Guide – Core Data Programming Guide

Multi-Context CoreData – Cocoanetics

Core Data Growing Pains

Synchronization – Threading Programming Guide

Thread Safety Summary – Threading Programming Guide