multithreading executeFetchRequest 上的“集合在枚举时发生了变异”

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/3446983/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-10 01:09:54  来源:igfitidea点击:

"Collection was mutated while being enumerated" on executeFetchRequest

multithreadingcocoacore-data

提问by Eric MORAND

I'm stuck on a problem for hours now and having read everything about this on stackoverflow (and apply every advices found), I'm now officially in need for help. ;o)

我现在被一个问题困住了几个小时,并且在 stackoverflow 上阅读了有关此的所有内容(并应用了找到的所有建议),我现在正式需要帮助。;o)

Here is the context :

这是上下文:

In my iPhone project, I need to import data on the background and insert it in a managed object context. Following the advices found here, here is what I'm doing :

在我的 iPhone 项目中,我需要在后台导入数据并将其插入到托管对象上下文中。按照此处找到的建议,这就是我正在做的事情:

  • Save the main moc
  • Instantiate a background moc with the persistent store coordinator used by the main moc
  • Register my controller as an observer of the NSManagedObjectContextDidSaveNotification notification for the background moc
  • Call the import method on a background thread
  • Each time data is received, insert it on the background moc
  • Once all the data has been imported, save the background moc
  • Merge the changes into the main moc, on the main thread
  • Unregister my controller as an observer for the notification
  • Reset and release the background moc
  • 保存主模型
  • 使用主 moc 使用的持久存储协调器实例化后台 moc
  • 将我的控制器注册为后台 moc 的 NSManagedObjectContextDidSaveNotification 通知的观察者
  • 在后台线程上调用导入方法
  • 每次接收到数据时,将其插入到后台moc
  • 导入所有数据后,保存后台 moc
  • 在主线程上将更改合并到主 moc 中
  • 取消将我的控制器注册为通知的观察者
  • 重置并释放后台 moc

Sometimes (and randomly), the exception...

有时(并且随机),例外...

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

...is thrown when I call executeFetchRequest on the background moc, to check if the imported data already exists in the database. I wonder what is mutating the set since there is nothing that run outside the import method.

...当我在后台 moc 上调用 executeFetchRequest 时抛出,以检查导入的数据是否已存在于数据库中。我想知道是什么改变了集合,因为在导入方法之外没有运行任何东西。

I've included the entire code of my controller and my test entity (my project consisting of these two classes and the app delegate, which has been unmodified) :

我已经包含了我的控制器和我的测试实体的完整代码(我的项目由这两个类和未修改的应用程序委托组成):

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

This is all ! The whole project is here. No table view, no NSFetchedResultsController, nothing else than a background thread that import data on a background moc.

这就是全部 !整个项目都在这里。没有表视图,没有 NSFetchedResultsController,没有别的,只是一个在后台 moc 上导入数据的后台线程。

What could mutate the set in this case ?

在这种情况下,什么可以改变集合?

I'm pretty sure I'm missing something obvious and it's driving me mad.

我很确定我遗漏了一些明显的东西,这让我很生气。

EDIT:

编辑:

Here is the full stack trace :

这是完整的堆栈跟踪:

    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'

回答by Eric MORAND

OK, I think I've solved my problem and I must thank this blog post from Fred McCann's :

好的,我想我已经解决了我的问题,我必须感谢 Fred McCann 的这篇博文:

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

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

The problem seems to come from the fact that I instantiate my background moc on the main thread instead of the background thread. When Apple tells that each thread needs to have its own moc, you have to take it seriously : each moc must be instantiated in the thread that will be using it !

问题似乎来自这样一个事实,即我在主线程而不是后台线程上实例化了我的后台 moc。当 Apple 告诉每个线程需要有自己的 moc 时,你必须认真对待:每个 moc 必须在将使用它的线程中实例化!

Moving the following lines...

移动以下几行...

// We instantiate the background moc

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

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

...in the _importData method (just before to register the controller as observer for the notification) solves the problem.

...在 _importData 方法中(就在将控制器注册为通知观察者之前)解决了这个问题。

Thanks for your help, Peter. And thanks to Fred McCann's for its valuable blog post !

谢谢你的帮助,彼得。并感谢 Fred McCann 的宝贵博客文章!

回答by Gagan_iOS

I was working on importing of record & display of records in tableview. Faced same issue when I tried to save record on backgroundThread like below

我正在研究在 tableview 中导入记录和显示记录。当我尝试在 backgroundThread 上保存记录时遇到同样的问题,如下所示

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

while I already created a PrivateQueueContext. Just replace above code with below one

虽然我已经创建了一个 PrivateQueueContext。只需用下面的代码替换上面的代码

[self saveObjectContextInDataBaseWithContext:privateQueueContext];

Really it was my foolish work to save on background thread while I already created a privateQueueConcurrencyType for saving record.

在我已经创建了一个用于保存记录的 privateQueueConcurrencyType 时,在后台线程上保存确实是我的愚蠢工作。