“A coleção sofreu uma mutação ao ser enumerada” em executeFetchRequest

Eu estou preso em um problema por horas agora e tendo lido tudo sobre isso em stackoverflow (e aplicar todos os avisos encontrados), estou agora oficialmente em necessidade de ajuda. o)

Aqui está o contexto:

No meu projeto do iPhone, preciso importar dados no plano de fundo e inseri-los em um contexto de object gerenciado. Seguindo os conselhos encontrados aqui, aqui está o que estou fazendo:

  • Salve o principal moc
  • Instanciar um background moc com o coordenador de armazenamento persistente usado pelo principal moc
  • Registrar meu controlador como um observador da notificação NSManagedObjectContextDidSaveNotification para o moc de segundo plano
  • Chame o método de importação em um thread de segundo plano
  • Cada vez que os dados são recebidos, insira-os no fundo
  • Depois que todos os dados forem importados, salve o fundo moc
  • Mesclar as alterações no principal moc, no thread principal
  • Cancelar o registro do meu controlador como um observador da notificação
  • Redefinir e liberar o fundo moc

Às vezes (e aleatoriamente), a exceção …

*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection  was mutated while being enumerated... 

… é lançado quando eu chamo executeFetchRequest no background moc, para verificar se os dados importados já existem no database. Eu me pergunto o que está alterando o conjunto desde que não há nada que seja executado fora do método de importação.

Eu incluí o código inteiro do meu controlador e minha entidade de teste (meu projeto consiste dessas duas classs e o representante do aplicativo, que não foi modificado):

 // // RootViewController.h // FK1 // // Created by Eric on 09/08/10. // Copyright (c) 2010 __MyCompanyName__. All rights reserved. // #import  @interface RootViewController : UITableViewController  { 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  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  @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 

Isso é tudo ! Todo o projeto está aqui. Sem exibição de tabela, sem NSFetchedResultsController, nada mais que um thread em segundo plano que importa dados em um background moc.

O que poderia mudar o conjunto neste caso?

Tenho certeza que estou sentindo falta de algo óbvio e isso está me enlouquecendo.

EDITAR:

Aqui está o rastreio da pilha completa:

  2010-08-10 10:29:11.258 FK1[51419:1b6b] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection  was mutated while being enumerated.{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' 

OK, acho que resolvi meu problema e devo agradecer a este post de Fred McCann:

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

O problema parece vir do fato de eu instanciar meu background moc no thread principal em vez do thread em segundo plano. Quando a Apple diz que cada thread precisa ter seu próprio moc, você tem que levar isso a sério: cada moc deve ser instanciado no thread que irá usá-lo!

Movendo as seguintes linhas …

 // We instantiate the background moc self.backgroundMOC = [[[NSManagedObjectContext alloc] init] autorelease]; [self.backgroundMOC setPersistentStoreCoordinator:[self.managedObjectContext persistentStoreCoordinator]]; 

… no método _importData (antes de registrar o controlador como observador para a notificação) resolve o problema.

Obrigado pela sua ajuda, Peter. E obrigado ao Fred McCann por seu valioso post no blog!

Eu estava trabalhando na importação de registro e exibição de registros em tableview. Enfrentou mesmo problema quando eu tentei salvar o registro no backgroundThread como abaixo

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

enquanto eu já criei um PrivateQueueContext. Basta replace o código acima por um abaixo

 [self saveObjectContextInDataBaseWithContext:privateQueueContext]; 

Realmente, foi meu trabalho tolo salvar o thread de plano de fundo enquanto eu já criei um privateQueueConcurrencyType para salvar o registro.