"La raccolta è stata modificata durante l'enumerazione" in executeFetchRequest


121

Sono bloccato su un problema da ore ormai e dopo aver letto tutto su questo su stackoverflow (e applicare tutti i consigli trovati), ora ho ufficialmente bisogno di aiuto. ; O)

Ecco il contesto:

Nel mio progetto iPhone, ho bisogno di importare i dati in background e inserirli in un contesto di oggetti gestiti. Seguendo i consigli che trovi qui, ecco cosa sto facendo:

  • Salva il file moc principale
  • Istanziare un moc in background con il coordinatore del negozio persistente utilizzato dal moc principale
  • Registra il mio controller come osservatore della notifica NSManagedObjectContextDidSaveNotification per il moc in background
  • Chiama il metodo di importazione su un thread in background
  • Ogni volta che si ricevono dati, inserirli nel moc di sfondo
  • Una volta che tutti i dati sono stati importati, salva lo sfondo moc
  • Unisci le modifiche nel moc principale, sul thread principale
  • Annulla la registrazione del mio controller come osservatore per la notifica
  • Ripristina e rilascia lo sfondo moc

A volte (e casualmente), l'eccezione ...

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

... viene generato quando chiamo executeFetchRequest sul moc in background, per verificare se i dati importati esistono già nel database. Mi chiedo cosa stia mutando il set poiché non c'è nulla che venga eseguito al di fuori del metodo di importazione.

Ho incluso l'intero codice del mio controller e della mia entità di test (il mio progetto composto da queste due classi e il delegato dell'app, che non è stato modificato):

//
//  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

Questo è tutto ! L'intero progetto è qui. Nessuna visualizzazione tabella, nessun NSFetchedResultsController, nient'altro che un thread in background che importa i dati su un moc in background.

Cosa potrebbe mutare il set in questo caso?

Sono abbastanza sicuro che mi manchi qualcosa di ovvio e mi sta facendo impazzire.

MODIFICARE:

Ecco la traccia completa dello stack:

    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'

2
Nel menu Esegui di Xcode, attiva "Stop on Objective-C Exceptions", quindi esegui la tua app nel Debugger. Cosa trovi?
Peter Hosey

1
Conferma che l'app si arresta in modo anomalo nella riga "executeFetchRequest: error:". Ho aggiunto la traccia dello stack completo alla mia domanda originale ...
Eric MORAND

E per quanto riguarda gli altri thread?
Peter Hosey

Hmmm, qui è il principale pila filo: # 0 0x958490fa a mach_msg_trap # 1 0x95849867 in mach_msg # 2 0x0253f206 a __CFRunLoopServiceMachPort # 3 0x0249c8b4 a __CFRunLoopRun # 4 0x0249c280 a CFRunLoopRunSpecific # 5 0x0249c1a1 in CFRunLoopRunInMode # 6 0x027a82c8 a GSEventRunModal # 7 0x027a838d a GSEventRun # 8 0x00021b58 in UIApplicationMain # 9 0x00001edc in main at main.m: 16 Ci sono altri 2 thread (libdispatch-manager e "WebThread") ma non forniscono ulteriori informazioni.
Eric MORAND

Risposte:


182

OK, penso di aver risolto il mio problema e devo ringraziare questo post sul blog di Fred McCann:

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

Il problema sembra derivare dal fatto che ho istanziato il mio moc in background sul thread principale anziché sul thread in background. Quando Apple dice che ogni thread deve avere il proprio moc, devi prenderlo sul serio: ogni moc deve essere istanziato nel thread che lo utilizzerà!

Spostamento delle seguenti righe ...

// We instantiate the background moc

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

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

... nel metodo _importData (subito prima di registrare il controller come osservatore per la notifica) risolve il problema.

Grazie per il tuo aiuto, Peter. E grazie a Fred McCann's per il suo prezioso post sul blog!


2
OK, dopo molti test, posso confermare che questo ha assolutamente risolto il mio problema. La contrassegnerò come risposta accettata non appena mi sarà permesso di ...
Eric MORAND

Grazie per questa soluzione! Per questa discussione c'è una buona implementazione di blocco / sblocco contesto di conflitti evitare durante la fusione: stackoverflow.com/questions/2009399/...
Gonso

4
+1 Grazie mille per aver posto la domanda, la soluzione e aver fornito il link al post del blog di Fred McCann .. Mi ha aiutato molto !!!
learner2010

3
each moc must be instantiated in the thread that will be using itHo pensato che solo l'operazione su MOC dovrebbe essere sullo stesso thread, ma creando anche il MOC stesso, se si tratta di un MOC privato, la relativa coda non esiste ancora ..
János

@ János Ho la stessa domanda qui. Come puoi istanziare il contesto nel thread che lo utilizzerà? Il thread non esiste ancora. Sto usando Swift e non capisco cosa significhi "spostamento nel metodo _importData".
Todanley

0

Stavo lavorando all'importazione di record e alla visualizzazione di record in tableview. Ho riscontrato lo stesso problema quando ho provato a salvare il record su backgroundThread come di seguito

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

mentre ho già creato un PrivateQueueContext. Basta sostituire il codice sopra con uno sotto

[self saveObjectContextInDataBaseWithContext:privateQueueContext];

In realtà è stato il mio lavoro sciocco salvare sul thread in background mentre creavo già un privateQueueConcurrencyType per il salvataggio del record.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.