"La colección fue mutada mientras se enumeraba" en executeFetchRequest

121

Estoy atrapado en un problema durante horas y después de leer todo sobre esto en stackoverflow (y aplicar todos los consejos encontrados), ahora estoy oficialmente necesitado de ayuda. ; o)

Aquí está el contexto:

En mi proyecto de iPhone, necesito importar datos en segundo plano e insertarlos en un contexto de objeto administrado. Siguiendo los consejos que se encuentran aquí, esto es lo que estoy haciendo:

  • Guarde el moc principal
  • Instancia un moc de fondo con el coordinador de tienda persistente utilizado por el moc principal
  • Registre mi controlador como observador de la notificación NSManagedObjectContextDidSaveNotification para el moc de fondo
  • Llame al método de importación en un hilo de fondo
  • Cada vez que se reciben datos, insértelos en el fondo moc
  • Una vez que se hayan importado todos los datos, guarde el moc de fondo
  • Combinar los cambios en el moc principal, en el hilo principal
  • Anular el registro de mi controlador como observador de la notificación
  • Restablecer y liberar el fondo moc

A veces (y al azar), la excepción ...

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

... se lanza cuando llamo executeFetchRequest en el fondo moc, para verificar si los datos importados ya existen en la base de datos. Me pregunto qué está mutando el conjunto ya que no hay nada que se ejecute fuera del método de importación.

He incluido el código completo de mi controlador y mi entidad de prueba (mi proyecto consiste en estas dos clases y el delegado de la aplicación, que no ha sido 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

Esto es todo ! Todo el proyecto está aquí. Sin vista de tabla, sin NSFetchedResultsController, nada más que un hilo de fondo que importa datos en un moc de fondo.

¿Qué podría mutar el conjunto en este caso?

Estoy bastante seguro de que me falta algo obvio y me está volviendo loco.

EDITAR:

Aquí está el seguimiento completo de la pila:

    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
fuente
2
En el menú Ejecutar de Xcode, active "Detener en Excepciones de Objective-C", luego ejecute su aplicación bajo el Depurador. ¿Qué encuentras?
Peter Hosey
1
Confirma que la aplicación se bloqueó en la línea "executeFetchRequest: error:". He agregado el seguimiento completo de la pila a mi pregunta original ...
Eric MORAND
¿Y qué hay de los otros hilos?
Peter Hosey
Hmmm, aquí es el principal pila hilo: # 0 0x958490fa en mach_msg_trap # 1 0x95849867 en mach_msg # 2 0x0253f206 en __CFRunLoopServiceMachPort # 3 0x0249c8b4 en __CFRunLoopRun # 4 0x0249c280 en CFRunLoopRunSpecific # 5 0x0249c1a1 en CFRunLoopRunInMode # 6 0x027a82c8 en GSEventRunModal # 7 0x027a838d en GSEventRun # 8 0x00021b58 en UIApplicationMain # 9 0x00001edc en main en main.m: 16 Hay otros 2 hilos (libdispatch-manager y "WebThread") pero no dan más información.
Eric MORAND

Respuestas:

182

OK, creo que he resuelto mi problema y debo agradecer esta publicación de blog de Fred McCann:

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

El problema parece provenir del hecho de que ejemplifico mi moc de fondo en el hilo principal en lugar del hilo de fondo. Cuando Apple dice que cada hilo necesita tener su propio moc, debes tomarlo en serio: ¡cada moc debe ser instanciado en el hilo que lo usará!

Moviendo las siguientes líneas ...

// We instantiate the background moc

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

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

... en el método _importData (justo antes de registrar el controlador como observador de la notificación) resuelve el problema.

Gracias por tu ayuda, Peter. ¡Y gracias a Fred McCann's por su valiosa publicación en el blog!

Eric MORAND
fuente
2
OK, después de muchas pruebas, puedo confirmar que esto resolvió absolutamente mi problema.
Marcaré
¡Gracias por esta solución! Este hilo tiene una muy buena implementación del contexto de bloqueo / desbloqueo para evitar conflictos durante la fusión: stackoverflow.com/questions/2009399/…
gonso
44
+1 Muchas gracias por plantear la pregunta, la solución y proporcionar el enlace a la publicación del blog de Fred McCann. ¡Me ayudó mucho!
Learner2010
3
each moc must be instantiated in the thread that will be using itPensé que solo la operación en MOC debería estar en el mismo hilo, pero crear el MOC también, si este es un MOC privado, la cola relacionada aún no existe ...
János
@ János tengo la misma pregunta aquí. ¿Cómo puedes instanciar el contexto en el hilo que lo usará? El hilo aún no existe. Estoy usando Swift y no entiendo qué significa "moverse en el método _importData".
Todanley
0

Estaba trabajando en la importación de registros y visualización de registros en vista de tabla. Enfrenté el mismo problema cuando intenté guardar el registro en segundo plano.

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

mientras que ya creé un PrivateQueueContext. Simplemente reemplace el código anterior por debajo de uno

[self saveObjectContextInDataBaseWithContext:privateQueueContext];

Realmente fue mi trabajo tonto guardar en el hilo de fondo mientras ya creé un privateQueueConcurrencyType para guardar el registro.

Gagan_iOS
fuente