"Koleksi telah dimutasi saat sedang dihitung" di executeFetchRequest

121

Saya terjebak pada masalah selama berjam-jam sekarang dan setelah membaca semua tentang ini di stackoverflow (dan menerapkan setiap saran yang ditemukan), saya sekarang secara resmi membutuhkan bantuan. ;Hai)

Inilah konteksnya:

Dalam proyek iPhone saya, saya perlu mengimpor data di latar belakang dan memasukkannya ke dalam konteks objek yang dikelola. Mengikuti saran yang ditemukan di sini, inilah yang saya lakukan:

  • Simpan moc utama
  • Membuat instance moc latar belakang dengan koordinator penyimpanan tetap yang digunakan oleh moc utama
  • Daftarkan pengontrol saya sebagai pengamat notifikasi NSManagedObjectContextDidSaveNotification untuk background moc
  • Panggil metode impor pada utas latar belakang
  • Setiap kali data diterima, masukkan pada background moc
  • Setelah semua data diimpor, simpan moc latar belakang
  • Gabungkan perubahan menjadi moc utama, di utas utama
  • Batalkan pendaftaran pengontrol saya sebagai pengamat untuk notifikasi
  • Setel ulang dan lepaskan moc latar belakang

Terkadang (dan secara acak), pengecualian ...

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

... dilempar ketika saya memanggil executeFetchRequest di background moc, untuk memeriksa apakah data yang diimpor sudah ada di database. Saya ingin tahu apa yang mengubah set karena tidak ada yang berjalan di luar metode impor.

Saya telah menyertakan seluruh kode pengontrol saya dan entitas pengujian saya (proyek saya terdiri dari dua kelas ini dan delegasi aplikasi, yang telah tidak diubah):

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

Ini semuanya! Seluruh proyek ada di sini. Tidak ada tampilan tabel, tidak ada NSFetchedResultsController, tidak ada yang lain selain utas latar belakang yang mengimpor data pada moc latar belakang.

Apa yang bisa mengubah himpunan dalam kasus ini?

Saya cukup yakin saya kehilangan sesuatu yang jelas dan itu membuat saya gila.

EDIT:

Berikut adalah pelacakan tumpukan lengkap:

    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
sumber
2
Di menu Jalankan Xcode, aktifkan "Berhenti di Pengecualian Objective-C", lalu jalankan aplikasi Anda di bawah Debugger. Apa yang kamu temukan?
Peter Hosey
1
Ini menegaskan bahwa aplikasi mogok pada baris "executeFetchRequest: error:". Saya telah menambahkan jejak tumpukan penuh ke pertanyaan asli saya ...
Eric MORAND
Dan bagaimana dengan utas lainnya?
Peter Hosey
Hmmm, di sini adalah benang utama stack: # 0 0x958490fa di mach_msg_trap # 1 0x95849867 di mach_msg # 2 0x0253f206 di __CFRunLoopServiceMachPort # 3 0x0249c8b4 di __CFRunLoopRun # 4 0x0249c280 di CFRunLoopRunSpecific # 5 0x0249c1a1 di CFRunLoopRunInMode # 6 0x027a82c8 di GSEventRunModal # 7 0x027a838d di GSEventRun # 8 0x00021b58 di UIApplicationMain # 9 0x00001edc di main di main.m: 16 Ada 2 utas lainnya (libdispatch-manager dan "WebThread") tetapi mereka tidak memberikan informasi lebih lanjut.
Eric MORAND

Jawaban:

182

Oke, saya pikir saya telah menyelesaikan masalah saya dan saya harus berterima kasih pada posting blog ini dari Fred McCann:

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

Masalahnya tampaknya berasal dari fakta bahwa saya memberi contoh moc latar belakang saya di utas utama, bukan utas latar belakang. Ketika Apple memberi tahu bahwa setiap utas harus memiliki moc sendiri, Anda harus menganggapnya serius: setiap moc harus dibuat di utas yang akan menggunakannya!

Memindahkan baris berikut ...

// We instantiate the background moc

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

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

... dalam metode _importData (sebelum mendaftarkan pengontrol sebagai pengamat untuk notifikasi) memecahkan masalah.

Terima kasih atas bantuan Anda, Peter. Dan terima kasih kepada Fred McCann untuk entri blognya yang berharga!

Eric MORAND
sumber
2
Oke, setelah banyak pengujian, saya dapat mengonfirmasi bahwa ini benar-benar menyelesaikan masalah saya. Saya akan menandai ini sebagai jawaban yang diterima segera setelah saya diizinkan untuk ...
Eric MORAND
Terima kasih untuk solusi ini! Utas ini memiliki implementasi konteks kunci / buka kunci yang sangat baik untuk menghindari konflik selama penggabungan: stackoverflow.com/questions/2009399/…
gonso
4
+1 Terima kasih banyak telah mengajukan pertanyaan, solusinya, dan memberikan tautan ke entri blog Fred McCann .. Itu sangat membantu saya !!!
pelajar2010
3
each moc must be instantiated in the thread that will be using itSaya meskipun hanya operasi di MOC yang harus berada di utas yang sama, tetapi membuat MOC itu sendiri juga, jika ini MOC pribadi, antrean terkait belum ada ..
János
@ János Saya memiliki pertanyaan yang sama di sini. Bagaimana Anda bisa membuat contoh konteks di utas yang akan menggunakannya? Utasnya belum ada. Saya menggunakan Swift dan saya tidak mengerti apa artinya "bergerak dalam metode _importData".
Todanley
0

Saya sedang mengerjakan mengimpor catatan & menampilkan catatan dalam tampilan tabel. Menghadapi masalah yang sama ketika saya mencoba menyimpan catatan di backgroundThread seperti di bawah ini

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

sementara saya sudah membuat PrivateQueueContext. Cukup ganti kode di atas dengan yang di bawah ini

[self saveObjectContextInDataBaseWithContext:privateQueueContext];

Sungguh, pekerjaan bodoh saya adalah menyimpan di utas latar belakang sementara saya sudah membuat privateQueueConcurrencyType untuk menyimpan catatan.

Gagan_iOS
sumber