Problème Core Data et background
Kubernan
Membre
Bonjour,
Avant de ne plus avoir un poil sur le caillou je vous expose mon problème.
Mon appli. iOS utilise intensément Core Data. Pour ne pas bloquer l'utilisateurs lors de certaines requêtes je jongle avec les dispatch_async et les concurrency type de mes contexts.
Tout allait bien jusqu'à ce que des problèmes de contentions surviennent au point de bloquer l'appli.
Du coup, j'ai opéré quelques changements qui résolvent ces problèmes mais rendent l'appli moins réactive.
Avec le recul je me dis que des choses ont du m'échapper dans le multithread avec Core Data, je compte sur vous pour m'éclairer.
Mon managed object context principal est de type NSMainQueueConcurrencyType. Comme son nom l'indique, ce context est connecté à la main queue. J'utilise donc ce context pour l'affichage des données dans mes view controllers.
Imaginons maintenant que mon appli débute par l'affichage d'une liste pour laquelle chaque item affiche une valeur récupérée par des fetchs dans Core Data.
Afin de ne pas bloquer l'interface je souhaite réaliser ces fetchs en arrière plan (je n'y fait pas de save).
Ma première idée fut d'utiliser le pattern suivant pour chacune des lignes de la liste :
Ce pattern fonctionne pour la première ligne à afficher, mais dès que je traite la seconde ligne de mon interface... paf.. il arrive que le fetch se fige (pas tout le temps). Quand tout se passe bien, l'affichage est fluide, le recyclage des cells se fait sans saccade. Mais la bonne exécution demeure aléatoire.
À l'évidence ce n'est pas la bonne méthode.
Pourquoi ? Je me demande si ce n'est pas dû au fait que le context gère sa propre queue ?
Donc, puisque ce context gère sa propre queue je me suis dit que j'allais utiliser les performBlock: du context. Ce performBlock: est censé être asynchrone.
J'ai remplacé le pattern précédent par :
Là je n'ai plus de problème de contention, mais l'affichage est moins fluide et provoque des saccades lors du recyclage des cellules.
Vous avez une idée de comment je peux gérer au mieux un affichage fluide et sûr tout en faisant des requêtes sur Core Data ?
Merci.
Avant de ne plus avoir un poil sur le caillou je vous expose mon problème.
Mon appli. iOS utilise intensément Core Data. Pour ne pas bloquer l'utilisateurs lors de certaines requêtes je jongle avec les dispatch_async et les concurrency type de mes contexts.
Tout allait bien jusqu'à ce que des problèmes de contentions surviennent au point de bloquer l'appli.
Du coup, j'ai opéré quelques changements qui résolvent ces problèmes mais rendent l'appli moins réactive.
Avec le recul je me dis que des choses ont du m'échapper dans le multithread avec Core Data, je compte sur vous pour m'éclairer.
Mon managed object context principal est de type NSMainQueueConcurrencyType. Comme son nom l'indique, ce context est connecté à la main queue. J'utilise donc ce context pour l'affichage des données dans mes view controllers.
Imaginons maintenant que mon appli débute par l'affichage d'une liste pour laquelle chaque item affiche une valeur récupérée par des fetchs dans Core Data.
Afin de ne pas bloquer l'interface je souhaite réaliser ces fetchs en arrière plan (je n'y fait pas de save).
Ma première idée fut d'utiliser le pattern suivant pour chacune des lignes de la liste :
<br />
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{<br />
// Je realise mes fetch de calcul avec le context courant<br />
dispatch_async(dispatch_get_main_queue(), ^{<br />
// Je mets à jour l'interface avec les résultats obtenus <br />
});<br />
}<br />
Ce pattern fonctionne pour la première ligne à afficher, mais dès que je traite la seconde ligne de mon interface... paf.. il arrive que le fetch se fige (pas tout le temps). Quand tout se passe bien, l'affichage est fluide, le recyclage des cells se fait sans saccade. Mais la bonne exécution demeure aléatoire.
À l'évidence ce n'est pas la bonne méthode.
Pourquoi ? Je me demande si ce n'est pas dû au fait que le context gère sa propre queue ?
Donc, puisque ce context gère sa propre queue je me suis dit que j'allais utiliser les performBlock: du context. Ce performBlock: est censé être asynchrone.
J'ai remplacé le pattern précédent par :
<br />
[self.managedObjectContext performBlock:^void {<br />
// Mes fetch<br />
// Mise a jour de mon interface car mon context est connecte a la main queue<br />
}];<br />
Là je n'ai plus de problème de contention, mais l'affichage est moins fluide et provoque des saccades lors du recyclage des cellules.
Vous avez une idée de comment je peux gérer au mieux un affichage fluide et sûr tout en faisant des requêtes sur Core Data ?
Merci.
Mots clés:
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
Plus d'info par ici : http://stackoverflow.com/questions/8305227/core-data-background-fetching-via-new-nsprivatequeueconcurrencytype
J'ai déjà testé les nested context. Les résultats obtenus sont assez proches de l'usage d'un simple context de type NSMainQueueConcurrencyType et son performBlock: Je pense que les avantages d'un nested context sont surtout : propagation automatique des changements vers le context parent (sans nécessité l'usage de notifications), possibilité d'annuler des modifs simplement en balançant la context enfant.
Tout cas j'ai pas constaté d'amélioration flagrante à l'usage d'un nested context. Ou alors je m'y prends comme un manche, c'est aussi possible.
En revanche j'ai obtenu des résultats bien supérieurs avec de nouveaux essais, simplement en retournant chez maman. C'est à dire en utilisant l'"ancien" type de context (NSConfinmentConcurrencyType) qu'on obtient automatiquement par un NSManagedObjectContext *context = [NSManagedobjectContext alloc] init]. En effet, je me suis dit que quitte à chercher les performances sur un des fetchs, autant leur dédier carrément un context.
Ainsi, mon pattern devient :
Pour l'instant je n'ai plus de problème de contention ni aucune saccade lors du recyclage des cells.
Bien vu ! Je me rappelais d'un post qui aurait pu m'aider.. mais je n'arrivais plus à m'en souvenir.... enfin, jme comprends :-)
Je vais voir ça.
Voir le tout début de la doc "Concurrency with Core Data".
Après, ce qui va poser un soucis c'est de créer un thread+un context par ligne. Je ne sais pas combien il y a de ligne mais je pense qu'il vaudrait mieux créer un seul thread d'arrière plan pour toutes les lignes.
De toutes façons si tous les contexts attaquent le même store, il y aura un lock pour l'accès au store et donc tu ne gagneras peut-être pas grand chose à trop multiplier les threads.
Je vais sembler méchant, mais as-tu lu les interventions précédente et les codes proposé avant de poster cette réponse ?
Effectivement, je n'avais pas lu tous les liens postés (je veux bien aider quand je sais mais je ne veux pas non plus y passer des heures).
Donc, après avoir lu les liens postés, je maintiens ma réponse. Et je pense même qu'elle est plus pertinente que les réponses précédentes.
La règle de base est bien un context par thread. Je cite la doc :
"The pattern recommended for concurrent programming with Core Data is thread confinement: each thread must have its own entirely private managed object context."
Et, quand on fait les choses à l'ancienne, on doit même créer le context à l'intérieur des threads et non pas le leur passer après leur création (ce que je ne savais pas, merci)
"(...) a context assumes the default owner is the thread or queue that allocated it"this is determined by the thread that calls its init method. You should not, therefore, initialize a context on one thread then pass it to a different thread"
Après effectivement, il y a eu des simplifications dans iOS 5.
Il reste que si on ne connait pas la règle de base, ces "simplifications" peuvent embrouiller les esprits.
D'ailleurs on voit que dans le cas décrit ici, si cela tombe en marche, c'est je pense (à vérifier) parce que l'utilisation de performblock sur le context de la main queue, ne fait qu'exécuter le block plus tard dans la même main queue, d'où des performances qui ne sont pas tellement meilleure.
Vis-à -vis du threading, les nested contexts permettent simplement de ne pas avoir à créer les threads soi-même. Cela ne règle pas le problème, c'est juste moins de code à écrire. Autrement dit :
"NSPrivateQueueConcurrencyType does not automagically make NSManagedObjectContext thread-safe. If you need to use Core Data on multiple threads, you should still use separate contexts for each thread."
(http://stackoverflow...t-context-on-tw)
Le code à taper est donc bien celui qu'on peut trouver dans le lien que tu as fourni :
NSManagedObjectContext *child = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[child setParentContext:self.managedObjectContext];
[child performBlock:^{
self.myDownloadClass = [[MyDownloadClass alloc]initInManagedObjectContext:child];
[self.myDownloadClass download];
}];
Puisque je viens d'y passer un peu de temps, voici ce que j'ai compris sur les différents mode de concurrence des contexts :
- NSConfinementConcurrencyType //Valeur par défaut, pour gérer soi-même comme avant ses contexts et ses threads (le mode qui permet de rester compatible avec iOS<5)
- NSPrivateQueueConcurrencyType //Context qui va gérer sa propre thread. Attention à bien passer par performBlock pour l'utiliser.
- NSMainQueueConcurrencyType //idem ci-dessus mais pour le thread principal. Du coup on est sans doute pas obligé d'utiliser performBlock pour le code exécuté par le thread principal.
Je parlais un peu plus haut des performances sur les nested context.
Je joins à ce post deux captures d'écrans du Time Profiler :
La capture intitulée "ThreadDispatchContext" correspond au pattern que j'ai finalement adopté à savoir :
La seconde capture (PerformBlock:) correspond au pattern d'un nested context.
Ce que l'on constate avec le nested context c'est qu'il y a bien un thread de créé mais on remarque que la partie "executeFetchRequest:" se réalise dans le main thread...
Alors effectivement, dans mon cas où j'ai un calcul pour chaque cell (rassurez-vous j'ai à peine 6 cells) ça saccade.
Oui c'est bien ce que j'avais supposé. Sauf que la cause n'est pas les "nested contexts" en soi.
C'est juste que tu sembles l'avoir mal implémenté. Le code correct c'est :
En gros, avant on devait d'abord crée un thread et, ensuite à l'intérieur du thread on devait créer un context et lui attribuer le store.
Pour simplifier, iOS5 renverse la manoeuvre, tu crée un context en lui disant qu'il va gérer son propre thread et au lieu de lui passer le store, tu lui passes le context principal.
D'ailleurs tu pourrais lui passer un store comme avant, je pense que cela marcherait aussi. C'est juste qu'avec les nested context, tu as en plus un report automatique des modifs quand tu fais un save dans le context child, ce qui n'est pas ton cas.
Bref, de toutes façons, ça ne donnera pas de meilleur résultat que le pattern que tu as finalement adopté, et qui semble correct, c'est juste une autre manière de l'écrire.
Mon code soumis au Time Profile pour le nested context est bien identique à celui que tu indiques. Je l'ai même vérifié en voyant les résultats.
Tu n'aurais pas une démo à partager pour qu'on puisse jouer dessus et voir si on arrive à quelque chose ?
Je joins un projet XCode qui reproduit mes observations.
XCode 4.3.2, ARC, Core Data et Storyboard
Normallement il n'y a qu'à faire un Run et changer le #define USE_COREDATA_NESTEDCONTEXT pour exécuter avec ou sans nested context.
Oops.. voici le projet...
Comme je n'y croyais pas, j'ai vérifié de mon côté avec un projet en cours.
Effectivement la grosse différence entre la solution avec nested context et la solution avec les contexts en "thread confinement", c'est que le "executeFetchRequest" est exécuté dans la main thread dans le premier cas.
Du coup les nested context perdent de l'intérêt pour faire des fetch d'arrière plan.
Après cela reste intéressant pour faire des modifications sur de nombreux objets en arrière plan.
D'un autre côté la notion de fetch d'arrière plan est assez limité quand on sait que normalement on ne doit pas passer les NSManagedObject récupérés d'un thread à un autre. On doit plutôt passer les objectID des objets récupérés vers la thread principale puis obtenir les objets à partir de leur objectID dans le context de la thread principal. Je pense que cela peut vite annuler le gain d'avoir un fetch en background.
En gros, en cas de nested, il faut se dire que tous les accès au store sont gérés par le thread du context principal . Aussi bien pour lire, que pour écrire dans le store. La doc ne le précise pas explicitement, il y a juste une phrase qui pourrait être plus clair sur l'aspect multi-thread :
Dans http://developer.app...ata/_index.html :
"Rather than specifying a persistent store coordinator for a managed object context, you can now specify a parent managed object context using . This means that fetch and save operations are mediated by the parent context instead of a coordinator. (...)"
Dans la session 303 de la dernière WWDC, à la partie Why Use Nested Context ? il y a 3 points dont :
- Background Fetching
J'utilise ce mécanisme dans d'autres parties de mon projet, j'ai pas constaté ce soucis.
Il en reste 2.
Lequel "ce" ?
Je reviens sur ce thread car j'ai plusieurs remarques qui peuvent êut-être aider ceux qui font du multi-threading avec Core Data.
D'abord le code suivant est à déconseiller car le context n'est pas crée et initialiser dans le même thread.
NSManagedObjectContext *context = [/color][color=#660066]NSManagedObjectContext[/color][color=#000000] alloc[/color][color=#666600 init];
context.persistentStoreCoordinator = self.managedObjectContext.persistentStoreCoordinator;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Mes calculs par fetch
dispatch_sync(dispatch_get_main_queue() , ^{
// Affichage des calculs
});
});
Le code suivant me semble OK car le context va être utilisé une seule fois dans la queue :
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSManagedObjectContext *context = [/color][color=#660066]NSManagedObjectContext[/color][color=#000000] alloc[/color][color=#666600 init];
context.persistentStoreCoordinator = self.managedObjectContext.persistentStoreCoordinator;
// Mes fetchs...
dispatch_sync(dispatch_get_main_queue() , ^{
// Affichage des calculs
});
});
Mais attention, une variante qui consisterait à créer une queue et à réutiliser plusieurs fois le context dans la même queue serait fausse.
Même si la queue est de type serial (c'est-à -dire une seule opération à la fois).
Je me suis fait avoir car j'ai supposé sans m'en rendre compte que la serial queue correspondait à un seul thread.
En fait non, la queue peut très bien utiliser un thread différent par opération même si elles ne sont pas concurrentes et donc ça pose problème à Core Data.
Par ailleurs, j'ai visualisé la session 303, il y a un slide assez clair qui montre bien qu'en cas de Nested queues, tous les accès au store se font via le context parent. Les résultats constatés par Kubernan ne sont donc pas suprenant. Ce qui est surprenant c'est que dans un autre slide, on conseille d'utiliser les nested context pour faire du background fetch.
Enfin, le pattern que j'ai adopté est le suivant :
NSManagedObjectContext *context = [/color][color=#660066]NSManagedObjectContext[/color][color=#000000] alloc[/color][color=#666600initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context performBlock:^{
context.persistentStoreCoordinator = self.persistentStoreCoordinator;
//Fetchs + calculs
}];
Dans ce cas, les fetchs ont bien lieu en arrière plan. Et je n'ai pas besoin de gérer les threads moi-même.
Je peux utiliser en parallèle un autre context configuré en NSMainQueueConcurrencyType.
La fonction des nestead context n'est absolument pas de faire des opérations en arrière pour optimiser sans se faire chier avec les thread.
Le but des nestead context est de se créer une page de brouillon lors d'édition d'un subset d'objet. L'exemple typique c'est iCal, l'application dispose d'un moc pour la fenêtre principale et chaque panel ouvert lors d'un clic sur un event dispose de son propre nestead moc. De cette manière, l'interface peut travail sur le moc pour mettre en place tous les changements souhaité avec validation de donnée & cie puis commiter tous les changement d'un coup dans le contexte d'origine (ou tout effacer sans commiter si l'utilisateur annule).
Le truc est que le nestead moc, lors d'un fetch, va locker le contexte parant ainsi que le store coordinator le temps de l'opération. Si l'on souhaite faire du fetch optimisé il faut le faire comme avant, dans un thread secondaire, récupérer les objectID puis les passer au thread d'origine pour récupérer le fault associé.
Voilà pour les précisions.
Merci pour ton retour WWDC Yoann! Comme quoi c'est un réel plus de pouvoir discuter directement avec les équipes /smile.png' class='bbc_emoticon' alt=':)' />
C'est effectivement ce que je fais dans mon appli. où j'ai des évènements répétitifs à gérer (hors calendrier). Pour les éditer j'utilise un nested context.
C'est bien le problème qui avait été soulevé alors que la présentation de l'année dernière parlait d'une utilisation des nested context pour des fetchs en background.
Merci beaucoup pour ton feedback !