“A coleção foi modificada enquanto estava sendo enumerada” em executeFetchRequest

121

Estou preso a um problema há horas e, depois de ler tudo sobre isso no stackoverflow (e aplicar todos os conselhos encontrados), agora estou oficialmente precisando de ajuda. ; o)

Aqui está o contexto:

No meu projeto do iPhone, preciso importar dados em segundo plano e inseri-los em um contexto de objeto gerenciado. Seguindo os conselhos encontrados aqui, eis o que estou fazendo:

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

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

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

... é acionado quando chamo executeFetchRequest no background moc, para verificar se os dados importados já existem no banco de dados. Gostaria de saber o que está mudando o conjunto, pois não há nada que funcione fora do método de importação.

Incluí todo o código do meu controlador e da minha entidade de teste (meu projeto consiste nessas duas classes e no delegado 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 <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

Isso é tudo ! Todo o projeto está aqui. Nenhuma visualização de tabela, nenhum NSFetchedResultsController, nada além de um encadeamento em segundo plano que importa dados em um moc em segundo plano.

O que poderia mudar o conjunto nesse caso?

Tenho certeza de que estou perdendo algo óbvio e isso está me deixando louco.

EDITAR:

Aqui está o rastreamento completo da pilha:

    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
fonte
2
No menu Executar do Xcode, ative "Parar em exceções de Objective-C" e execute seu aplicativo no Debugger. O que você encontrou?
Peter Hosey
1
Ele confirma que o aplicativo travou na linha "executeFetchRequest: error:". Eu adicionei o rastreamento de pilha completo para a minha pergunta original ...
Eric MORAND
E quanto aos outros tópicos?
Peter Hosey
Hum, aqui, é a principal pilha de rosca: # 0 0x958490fa em mach_msg_trap # 1 0x95849867 em mach_msg # 2 0x0253f206 em __CFRunLoopServiceMachPort # 3 0x0249c8b4 em __CFRunLoopRun # 4 0x0249c280 em CFRunLoopRunSpecific # 5 0x0249c1a1 em CFRunLoopRunInMode # 6 0x027a82c8 em GSEventRunModal # 7 0x027a838d em GSEventRun # 8 0x00021b58 no UIApplicationMain # 9 0x00001edc no main em main.m: 16 Existem outros 2 threads (libdispatch-manager e "WebThread"), mas eles não fornecem mais informações.
Eric MORAND

Respostas:

182

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 de background. Quando a Apple diz que cada thread precisa ter seu próprio moc, você deve levar isso a sério: cada moc deve ser instanciado no thread que o usará!

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 (imediatamente antes de registrar o controlador como observador da notificação) resolve o problema.

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

Eric MORAND
fonte
2
OK, depois de muitos testes, posso confirmar que isso resolveu absolutamente o meu problema. Vou marcar este como resposta aceita, logo que eu tenho permissão para ...
Eric MORAND
Obrigado por esta solução! Esta discussão tem uma boa implementação de bloqueio de contexto / desbloqueio para conflitos evitar durante merge: stackoverflow.com/questions/2009399/...
Gonso
4
+1 Muito obrigado por colocar a pergunta, a solução e fornecer o link para o post de Fred McCann no blog. Isso me ajudou muito !!!
learner2010
3
each moc must be instantiated in the thread that will be using itI embora apenas a operação em MOC deve estar no mesmo segmento, mas a criação do próprio MOC também, se este é um MOC privada, a fila relacionada não é ainda existe ..
János
@ János eu tenho a mesma pergunta aqui. Como você pode instanciar o contexto no thread que o usará? O encadeamento ainda não existe. Estou usando o Swift e não entendo o que significa "mover no método _importData".
Todanley
0

Eu estava trabalhando na importação de registro e exibição de registros em tableview. Enfrentou o mesmo problema quando tentei salvar o registro em segundo plano

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

enquanto eu já criei um PrivateQueueContext. Apenas substitua o código acima por um

[self saveObjectContextInDataBaseWithContext:privateQueueContext];

Realmente, foi meu trabalho tolo salvar no encadeamento em segundo plano enquanto eu já criava um privateQueueConcurrencyType para salvar o registro.

Gagan_iOS
fonte