"La collection a subi une mutation lors de son énumération" sur executeFetchRequest

121

Je suis coincé sur un problème depuis des heures maintenant et après avoir tout lu à ce sujet sur stackoverflow (et appliquer tous les conseils trouvés), j'ai maintenant officiellement besoin d'aide. ; o)

Voici le contexte:

Dans mon projet iPhone, j'ai besoin d'importer des données en arrière-plan et de les insérer dans un contexte d'objet géré. En suivant les conseils trouvés ici, voici ce que je fais:

  • Enregistrer le moc principal
  • Instanciez un moc d'arrière-plan avec le coordinateur de magasin persistant utilisé par le moc principal
  • Enregistrer mon contrôleur en tant qu'observateur de la notification NSManagedObjectContextDidSaveNotification pour le moc d'arrière-plan
  • Appelez la méthode d'importation sur un thread d'arrière-plan
  • Chaque fois que des données sont reçues, insérez-les sur le fond d'écran
  • Une fois toutes les données importées, enregistrez le moc d'arrière-plan
  • Fusionner les modifications dans le moc principal, sur le thread principal
  • Annuler l'enregistrement de mon contrôleur en tant qu'observateur pour la notification
  • Réinitialiser et relâcher le moc d'arrière-plan

Parfois (et au hasard), l'exception ...

*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSCFSet: 0x5e0b930> was mutated while being enumerated...

... est lancé lorsque j'appelle executeFetchRequest sur le moc d'arrière-plan, pour vérifier si les données importées existent déjà dans la base de données. Je me demande ce qui fait muter l'ensemble car il n'y a rien qui fonctionne en dehors de la méthode d'importation.

J'ai inclus le code complet de mon contrôleur et de mon entité de test (mon projet composé de ces deux classes et du délégué d'application, qui n'a pas été modifié):

//
//  RootViewController.h
//  FK1
//
//  Created by Eric on 09/08/10.
//  Copyright (c) 2010 __MyCompanyName__. All rights reserved.
//


#import <CoreData/CoreData.h>

@interface RootViewController : UITableViewController <NSFetchedResultsControllerDelegate> {
    NSManagedObjectContext *managedObjectContext;
    NSManagedObjectContext *backgroundMOC;
}


@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain) NSManagedObjectContext *backgroundMOC;

@end


//
//  RootViewController.m
//  FK1
//
//  Created by Eric on 09/08/10.
//  Copyright (c) 2010 __MyCompanyName__. All rights reserved.
//


#import "RootViewController.h"
#import "FK1Message.h"

@implementation RootViewController

@synthesize managedObjectContext;
@synthesize backgroundMOC;

- (void)viewDidLoad {
    [super viewDidLoad];

    self.navigationController.toolbarHidden = NO;

    UIBarButtonItem *refreshButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(refreshAction:)];

    self.toolbarItems = [NSArray arrayWithObject:refreshButton];
}

#pragma mark -
#pragma mark ACTIONS

- (void)refreshAction:(id)sender {
    // If there already is an import running, we do nothing

    if (self.backgroundMOC != nil) {
        return;
    }

    // We save the main moc

    NSError *error = nil;

    if (![self.managedObjectContext save:&error]) {
        NSLog(@"error = %@", error);

        abort();
    }

    // We instantiate the background moc

    self.backgroundMOC = [[[NSManagedObjectContext alloc] init] autorelease];

    [self.backgroundMOC setPersistentStoreCoordinator:[self.managedObjectContext persistentStoreCoordinator]];

    // We call the fetch method in the background thread

    [self performSelectorInBackground:@selector(_importData) withObject:nil];
}

- (void)_importData {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundMOCDidSave:) name:NSManagedObjectContextDidSaveNotification object:self.backgroundMOC];         

    FK1Message *message = nil;

    NSFetchRequest *fetchRequest = nil;
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"FK1Message" inManagedObjectContext:self.backgroundMOC];
    NSPredicate *predicate = nil;
    NSArray *results = nil;

    // fake import to keep this sample simple

    for (NSInteger index = 0; index < 20; index++) {
        predicate = [NSPredicate predicateWithFormat:@"msgId == %@", [NSString stringWithFormat:@"%d", index]];

        fetchRequest = [[[NSFetchRequest alloc] init] autorelease];

        [fetchRequest setEntity:entity];
        [fetchRequest setPredicate:predicate];

        // The following line sometimes randomly throw the exception :
        // *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSCFSet: 0x5b71a00> was mutated while being enumerated.

        results = [self.backgroundMOC executeFetchRequest:fetchRequest error:NULL];

        // If the message already exist, we retrieve it from the database
        // If it doesn't, we insert a new message in the database

        if ([results count] > 0) {
            message = [results objectAtIndex:0];
        }
        else {
            message = [NSEntityDescription insertNewObjectForEntityForName:@"FK1Message" inManagedObjectContext:self.backgroundMOC];
            message.msgId = [NSString stringWithFormat:@"%d", index];
        }

        // We update the message

        message.updateDate = [NSDate date];
    }

    // We save the background moc which trigger the backgroundMOCDidSave: method

    [self.backgroundMOC save:NULL];

    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:self.backgroundMOC];

    [self.backgroundMOC reset]; self.backgroundMOC = nil;

    [pool drain];
}

- (void)backgroundMOCDidSave:(NSNotification*)notification {    
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(backgroundMOCDidSave:) withObject:notification waitUntilDone:YES];
        return;
    }

    // We merge the background moc changes in the main moc

    [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}

@end

//
//  FK1Message.h
//  FK1
//
//  Created by Eric on 09/08/10.
//  Copyright 2010 __MyCompanyName__. All rights reserved.
//

#import <CoreData/CoreData.h>

@interface FK1Message :  NSManagedObject  
{
}

@property (nonatomic, retain) NSString * msgId;
@property (nonatomic, retain) NSDate * updateDate;

@end

// 
//  FK1Message.m
//  FK1
//
//  Created by Eric on 09/08/10.
//  Copyright 2010 __MyCompanyName__. All rights reserved.
//

#import "FK1Message.h"

@implementation FK1Message 

#pragma mark -
#pragma mark PROPERTIES

@dynamic msgId;
@dynamic updateDate;

@end

C'est tout ! L'ensemble du projet est ici. Pas de vue de table, pas de NSFetchedResultsController, rien d'autre qu'un thread d'arrière-plan qui importe des données sur un moc d'arrière-plan.

Qu'est-ce qui pourrait muter l'ensemble dans ce cas?

Je suis presque sûr que je rate quelque chose d'évident et cela me rend fou.

ÉDITER:

Voici la trace complète de la pile:

    2010-08-10 10:29:11.258 FK1[51419:1b6b] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSCFSet: 0x5d075b0> was mutated while being enumerated.<CFBasicHash 0x5d075b0 [0x25c6380]>{type = mutable set, count = 0,
entries =>
}
'
*** Call stack at first throw:
(
    0   CoreFoundation                      0x0255d919 __exceptionPreprocess + 185
    1   libobjc.A.dylib                     0x026ab5de objc_exception_throw + 47
    2   CoreFoundation                      0x0255d3d9 __NSFastEnumerationMutationHandler + 377
    3   CoreData                            0x02287702 -[NSManagedObjectContext executeFetchRequest:error:] + 4706
    4   FK1                                 0x00002b1b -[RootViewController _fetchData] + 593
    5   Foundation                          0x01d662a8 -[NSThread main] + 81
    6   Foundation                          0x01d66234 __NSThread__main__ + 1387
    7   libSystem.B.dylib                   0x9587681d _pthread_start + 345
    8   libSystem.B.dylib                   0x958766a2 thread_start + 34
)
terminate called after throwing an instance of 'NSException'
Eric MORAND
la source
2
Dans le menu Exécuter de Xcode, activez «Arrêter sur les exceptions Objective-C», puis exécutez votre application sous le débogueur. Que trouvez-vous?
Peter Hosey
1
Il confirme que l'application plante sur la ligne "executeFetchRequest: error:". J'ai ajouté la trace de pile complète à ma question d'origine ...
Eric MORAND
Et qu'en est-il des autres fils?
Peter Hosey
Hum, voici la pile principale du fil: # 0 0x958490fa dans mach_msg_trap # 1 0x95849867 dans mach_msg # 2 0x0253f206 dans __CFRunLoopServiceMachPort # 3 0x0249c8b4 dans __CFRunLoopRun # 4 0x0249c280 dans CFRunLoopRunSpecific # 5 0x0249c1a1 dans CFRunLoopRunInMode # 6 0x027a82c8 dans GSEventRunModal # 7 0x027a838d dans GSEventRun # 8 0x00021b58 dans UIApplicationMain # 9 0x00001edc dans main à main.m: 16 Il y a 2 autres threads (libdispatch-manager et "WebThread") mais ils ne donnent pas plus d'informations.
Eric MORAND

Réponses:

182

OK, je pense avoir résolu mon problème et je dois remercier ce billet de blog de Fred McCann:

http://www.duckrowing.com/2010/03/11/using-core-data-on-multiple-threads/

Le problème semble provenir du fait que j'instancie mon moc d'arrière-plan sur le thread principal au lieu du thread d'arrière-plan. Quand Apple dit que chaque thread doit avoir son propre moc, vous devez le prendre au sérieux: chaque moc doit être instancié dans le thread qui l'utilisera!

Déplacement des lignes suivantes ...

// We instantiate the background moc

self.backgroundMOC = [[[NSManagedObjectContext alloc] init] autorelease];

[self.backgroundMOC setPersistentStoreCoordinator:[self.managedObjectContext persistentStoreCoordinator]];

... dans la méthode _importData (juste avant d'enregistrer le contrôleur en tant qu'observateur pour la notification) résout le problème.

Merci pour votre aide, Peter. Et merci à Fred McCann pour son précieux article de blog!

Eric MORAND
la source
2
OK, après de nombreux tests, je peux confirmer que cela a résolu mon problème. Je marquerai cette réponse comme acceptée dès que je serai autorisé à ...
Eric MORAND
Merci pour cette solution! Ce thread a une très bonne implémentation du contexte de verrouillage / déverrouillage pour éviter les conflits lors de la fusion: stackoverflow.com/questions/2009399/…
gonso
4
+1 Merci beaucoup d'avoir posé la question, la solution et le lien vers le billet de blog de Fred McCann. Cela m'a beaucoup aidé !!!
apprenant2010
3
each moc must be instantiated in the thread that will be using itJe pensais que seule l'opération sur MOC devrait être sur le même thread, mais en créant le MOC lui-même aussi, s'il s'agit d'un MOC privé, la file d'attente associée n'existe pas encore ..
János
@ János J'ai la même question ici. Comment pouvez-vous instancier le contexte dans le thread qui l'utilisera? Le fil n'existe pas encore. J'utilise Swift et je ne comprends pas ce que signifie "se déplacer dans la méthode _importData".
Todanley
0

Je travaillais sur l'importation d'enregistrements et l'affichage d'enregistrements dans tableview. J'ai rencontré le même problème lorsque j'ai essayé d'enregistrer un enregistrement en arrière-plan

 [self performSelectorInBackground:@selector(saveObjectContextInDataBaseWithContext:) withObject:privateQueueContext];

alors que j'ai déjà créé un PrivateQueueContext. Remplacez simplement le code ci-dessus par un code ci-dessous

[self saveObjectContextInDataBaseWithContext:privateQueueContext];

C'était vraiment mon travail stupide de sauvegarder sur le fil d'arrière-plan alors que j'avais déjà créé un privateQueueConcurrencyType pour enregistrer un enregistrement.

Gagan_iOS
la source