22

J'utilise Grand Central Dispatch (GCD) dans mon application pour soulever des charges lourdes. L'application utilise Core-Data à des fins de stockage de données. Voici mon scénario (ainsi que des questions pertinentes):Grand Central Dispatch (GCD) avec CoreData

dispatch_queue_t main_queue = dispatch_get_main_queue(); 
dispatch_queue_t request_queue = dispatch_queue_create("com.app.request", NULL); 

dispatch_async(request_queue, ^{ 
    MyNSManagedObject *mObject = [self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; 

    // … 
    // <heavy lifting> 
    // … 

    // … 
    // <update mObject> 
    // … 

    [self saveManagedObjectContext]; 
});  

En raison de [self saveManagedObjectContext], fetchResultsController méthodes de délégués sont appelés automatiquement. Par conséquent, la logique de mise à jour de l'interface utilisateur se déclenche.

Maintenant, ma question est, dois-je utiliser main_queue pour -saveManagedObjectContext? Devrais-je effectuer toutes les opérations sur mon NSManagedObject en main_queue? Certaines des opérations qui mettent à jour le NSManagedObject peuvent prendre 2-3 secondes. S'il vous plaît donnez votre avis.

Répondre

17

Comme vous le savez probablement ou avez remarqué que vous devez effectuer des opérations de l'interface utilisateur sur le thread principal. Comme vous le mentionnez c'est quand vous enregistrez la mise à jour de l'interface utilisateur a lieu. Vous pouvez résoudre ce problème en imbriquant un appel au dispatch_sync sur le fil principal.

dispatch_queue_t main_queue = dispatch_get_main_queue(); 
dispatch_queue_t request_queue = dispatch_queue_create("com.app.request", NULL); 

__block __typeof__(self) blockSelf = self; 

dispatch_async(request_queue, ^{ 
    MyNSManagedObject *mObject = [blockSelf.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; 

    // update and heavy lifting... 

    dispatch_sync(main_queue, ^{ 
     [blockSelf saveManagedObjectContext]; 
    }); 
});  

L'utilisation deblockSelfest d'éviter de créer accidentellement référence cycles. (Practical blocks)

59

Il existe une règle d'or en ce qui concerne les données de base: un contexte d'objet géré par thread. Les contextes d'objets gérés ne sont pas thread-safe, donc si vous travaillez dans une tâche d'arrière-plan, utilisez le thread principal pour éviter les conflits de threads avec les opérations de l'interface utilisateur ou créez un nouveau contexte pour effectuer le travail. quelques secondes alors vous devriez faire le dernier pour empêcher votre UI de se bloquer.

Pour ce faire, vous créez un nouveau contexte et lui donner le même magasin persistant que votre contexte principal:

NSManagedObjectContext *backgroundContext = [[[NSManagedObjectContext alloc] init] autorelease]; 
[backgroundContext setPersistentStoreCoordinator:[mainContext persistentStoreCoordinator]]; 

Faites ce que les opérations que vous devez faire, lorsque vous enregistrez ce nouveau contexte que vous devez gérer la notification de sauvegarde et fusionner les modifications dans votre contexte principal avec le message mergeChangesFromContextDidSaveNotification:. Le code devrait ressembler à ceci:

/* Save notification handler for the background context */ 
- (void)backgroundContextDidSave:(NSNotification *)notification { 
    /* Make sure we're on the main thread when updating the main context */ 
    if (![NSThread isMainThread]) { 
     [self performSelectorOnMainThread:@selector(backgroundContextDidSave:) 
           withObject:notification 
          waitUntilDone:NO]; 
     return; 
    } 

    /* merge in the changes to the main context */ 
    [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification]; 
} 

/* ... */ 

/* Save the background context and handle the save notification */ 
[[NSNotificationCenter defaultCenter] addObserver:self 
             selector:@selector(backgroundContextDidSave:) 
              name:NSManagedObjectContextDidSaveNotification 
              object:backgroundContext]; 

[backgroundContext save:NULL]; 

[[NSNotificationCenter defaultCenter] removeObserver:self 
               name:NSManagedObjectContextDidSaveNotification 
               object:syncContext]; 

Manipulation la sauvegarde notifcation et la fusion est importante, sinon votre principale interface utilisateur/contexte ne verra pas les modifications apportées. En fusionnant, votre principal fetchResultsController etc. obtiendra des événements de changement et mettra à jour votre interface utilisateur comme vous vous y attendez.

Une autre chose importante à noter est que les instances de NSManagedObject ne peuvent être utilisées que dans le contexte d'où elles ont été extraites. Si votre opération nécessite une référence à un objet, vous devez passer le objectID de l'objet à l'opération et extraire une nouvelle instance NSManagedObject du nouveau contexte à l'aide de existingObjectWithID:. Donc, quelque chose comme:

/* This can only be used in operations on the main context */ 
MyNSManagedObject *objectInMainContext = 
    [self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; 

/* This can now be used in your background context */ 
MyNSManagedObject *objectInBackgroundContext = 
    (MyNSManagedObject *) [backgroundContext existingObjectWithID:[objectInMainContext objectID]]; 
+1

Donc, ce que vous dites est que dans mon cas, au lieu d'utiliser fetchedResultsController, je devrais créer un nouveau contexte d'objet géré (backgro undManagedObjectContext), récupère l'objet géré requis, effectue les opérations requises, met à jour l'objet géré, enregistre ce contexte d'objet géré (backgroundManagedObjectContext), puis fusionne les modifications pour refléter dans le contexte de l'objet géré principal. Cela rendra ma vie sacrément misérable. – Mustafa

+0

Vous pouvez toujours utiliser le contrôleur de résultats récupérés pour afficher et mettre à jour des éléments dans votre interface utilisateur. Le code que je vous ai montré est tout ce qui est nécessaire pour effectuer toutes les opérations dont vous avez besoin dans un contexte séparé, rien d'autre ne devrait changer. –

+0

Il existe des moyens de contourner cette règle d'or: https://github.com/adam-roth/coredata-threadsafe. – aroth

0

Depuis Core Data nécessite un Managed contexte de l'objet par thread, une solution possible serait de suivre un contexte par thread dans un gestionnaire global, puis suivre les enregistrer les notifications et se propager à toutes les discussions:

En supposant :

@property (nonatomic, strong) NSDictionary* threadsDictionary; 

Voici comment obtenir l'objet géré (par fil):

- (NSManagedObjectContext *) managedObjectContextForThread { 

// Per thread, give one back 
NSString* threadName = [NSString stringWithFormat:@"%d",[NSThread currentThread].hash]; 

NSManagedObjectContext * existingContext = [self.threadsDictionary objectForKey:threadName]; 
if (existingContext==nil){ 
    existingContext = [[NSManagedObjectContext alloc] init]; 
    [existingContext setPersistentStoreCoordinator: [self persistentStoreCoordinator]]; 
    [self.threadsDictionary setValue:existingContext forKey:threadName]; 
} 

return existingContext; 

}

À un certain moment dans la méthode d'initialisation de votre gestionnaire global (j'ai utilisé un singleton):

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundContextDidSave:)             name:NSManagedObjectContextDidSaveNotification             object:nil]; 

ensuite recevoir sauvegarder les notifications et se propager à tous les autres contexte objets gérés:

- (void)backgroundContextDidSave:(NSNotification *)notification { 
    /* Make sure we're on the main thread when updating the main context */ 
    if (![NSThread isMainThread]) { 
     [self performSelectorOnMainThread:@selector(backgroundContextDidSave:) 
           withObject:notification 
          waitUntilDone:NO]; 
     return; 
    } 

    /* merge in the changes to the main context */ 
    for (NSManagedObjectContext* context in [self.threadsDictionary allValues]){ 
      [context mergeChangesFromContextDidSaveNotification:notification]; 
    } 
} 

(d'autres méthodes ont été supprimées pour plus de clarté)