executeFetchRequest에서 "열거하는 동안 컬렉션이 변경되었습니다."


121

나는 몇 시간 동안 문제에 갇혀 있고 stackoverflow에서 이것에 대한 모든 것을 읽었고 (그리고 발견 된 모든 조언을 적용) 이제 공식적으로 도움이 필요합니다. ;영형)

컨텍스트는 다음과 같습니다.

내 iPhone 프로젝트에서 백그라운드에서 데이터를 가져 와서 관리되는 개체 컨텍스트에 삽입해야합니다. 여기에서 찾은 조언에 따라 내가하는 일은 다음과 같습니다.

  • 메인 moc 저장
  • 기본 moc에서 사용하는 영구 저장소 코디네이터를 사용하여 백그라운드 moc를 인스턴스화합니다.
  • 내 컨트롤러를 백그라운드 moc에 대한 NSManagedObjectContextDidSaveNotification 알림의 관찰자로 등록하십시오.
  • 백그라운드 스레드에서 가져 오기 메소드 호출
  • 데이터가 수신 될 때마다 배경 moc에 삽입
  • 모든 데이터를 가져 왔으면 배경 moc를 저장하십시오.
  • 메인 스레드의 메인 moc에 변경 사항을 병합합니다.
  • 알림에 대한 관찰자로 내 컨트롤러 등록을 취소합니다.
  • 배경 moc 재설정 및 해제

때때로 (그리고 무작위로) 예외 ...

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

... 가져온 데이터가 데이터베이스에 이미 있는지 확인하기 위해 백그라운드 moc에서 executeFetchRequest를 호출 할 때 발생합니다. 가져 오기 방법 외부에서 실행되는 것이 없기 때문에 세트를 변경하는 것이 무엇인지 궁금합니다.

컨트롤러와 테스트 엔터티의 전체 코드를 포함했습니다 (이 두 클래스와 수정되지 않은 앱 델리게이트로 구성된 프로젝트).

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

이게 다야 ! 전체 프로젝트가 여기에 있습니다. 테이블 뷰, NSFetchedResultsController, 백그라운드 moc에서 데이터를 가져 오는 백그라운드 스레드 외에는 아무것도 없습니다.

이 경우 세트를 변경할 수있는 것은 무엇입니까?

나는 분명한 것을 놓치고 있고 그것이 나를 미치게 만든다고 확신합니다.

편집하다:

다음은 전체 스택 추적입니다.

    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
Xcode의 실행 메뉴에서“Stop on Objective-C Exceptions”를 켠 다음 Debugger에서 앱을 실행합니다. 무엇을 찾았습니까?
— 피터 Hosey

1
"executeFetchRequest : error :"줄에서 앱 충돌을 확인합니다. 나는 ... 내 원래의 질문의 전체 스택 추적을 추가 한
— 에릭 MORAND에게

그리고 다른 스레드는 어떻습니까?
— Peter Hosey

Hmmm, 여기에 메인 스레드 스택이 있습니다. 8 0x00021b58 in UIApplicationMain # 9 0x00001edc in main at main.m : 16 2 개의 다른 스레드 (libdispatch-manager 및 "WebThread")가 있지만 더 많은 정보를 제공하지 않습니다.
— Eric MORAND

답변:


182

좋아, 나는 내 문제를 해결했다고 생각하며 Fred McCann의 다음 블로그 게시물에 감사드립니다.

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

문제는 백그라운드 스레드 대신 메인 스레드에서 백그라운드 moc을 인스턴스화한다는 사실에서 비롯된 것 같습니다. 애플이 각 스레드가 자체 moc를 가져야한다고 말할 때, 당신은 그것을 진지하게 받아 들여야합니다 : 각 moc는 그것을 사용할 스레드에서 인스턴스화되어야합니다!

다음 줄 이동 ...

// We instantiate the background moc

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

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

... _importData 메서드에서 (알림에 대한 관찰자로 컨트롤러를 등록하기 직전) 문제를 해결합니다.

도와 주셔서 감사합니다, 피터 그리고 소중한 블로그 게시물에 대한 Fred McCann에게 감사드립니다!


2
좋아, 많은 테스트를 거친 후 이것이 내 문제를 절대적으로 해결했는지 확인할 수 있습니다. 허용
— 되는대로이

이 솔루션에 감사드립니다! 이 스레드는 병합 중 충돌을 피하기 위해 잠금 / 잠금 해제 컨텍스트를 매우 잘 구현했습니다. stackoverflow.com/questions/2009399/…
— gonso

4
+1 질문과 해결책을 제시하고 Fred McCann의 블로그 게시물에 대한 링크를 제공해 주셔서 감사합니다. 많은 도움이되었습니다 !!!
— learner2010

3
each moc must be instantiated in the thread that will be using itMOC에 대한 작업 만 동일한 스레드에 있어야하지만 MOC 자체도 생성하지만 이것이 개인 MOC 인 경우 관련 대기열은 아직 존재하지 않습니다 ..
— János

@ János 여기에 같은 질문이 있습니다. 컨텍스트를 사용할 스레드에서 어떻게 인스턴스화 할 수 있습니까? 스레드가 아직 존재하지 않습니다. 저는 Swift를 사용하고 있는데 "moving in the _importData method"가 무엇을 의미하는지 이해하지 못합니다.
— Todanley

0

나는 테이블 뷰에서 레코드 가져 오기 및 레코드 표시 작업을하고있었습니다. 아래와 같이 backgroundThread에 레코드를 저장하려고 할 때 동일한 문제에 직면했습니다.

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

이미 PrivateQueueContext를 만들었습니다. 위의 코드를 아래 코드로 바꾸십시오.

[self saveObjectContextInDataBaseWithContext:privateQueueContext];

레코드를 저장하기 위해 이미 privateQueueConcurrencyType을 만들면서 백그라운드 스레드에 저장하는 것은 정말 어리석은 작업이었습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.