【问题标题】:"Collection was mutated while being enumerated" on executeFetchRequestexecuteFetchRequest 上的“集合在枚举时发生了变异”
【发布时间】:2011-03-27 16:40:21
【问题描述】:

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

这里是上下文:

在我的 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'

【问题讨论】:

  • 在 Xcode 的 Run 菜单中,打开“Stop on Objective-C Exceptions”,然后在 Debugger 下运行你的应用程序。你发现了什么?
  • 它确认应用程序在“executeFetchRequest:error:”行崩溃。我已将完整的堆栈跟踪添加到我原来的问题中......
  • 其他线程呢?
  • 嗯,这里是主线程堆栈:#0 0x958490fa在mach_msg_trap#1 0x95849867在mach_msg#2 0x0253f206在__CFRunLoopServiceMachPort#在__CFRunLoopRun 3 0x0249c8b4#4 0x0249c280在CFRunLoopRunSpecific#5 0x0249c1a1在CFRunLoopRunInMode#6 0x027a82c8 in GSEventRunModal #7 0x027a838d in GSEventRun #8 0x00021b58 in UIApplicationMain #9 0x00001edc in main at main.m:16 还有 2 个其他线程(libdispatch-manager 和“WebThread”),但它们没有提供更多信息。

标签: multithreading cocoa core-data


【解决方案1】:

我正在导入记录并在表格视图中显示记录。当我尝试在 backgroundThread 上保存记录时遇到同样的问题,如下所示

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

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

[self saveObjectContextInDataBaseWithContext:privateQueueContext];

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

【讨论】:

    【解决方案2】:

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

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

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

    移动以下几行...

    // We instantiate the background moc
    
    self.backgroundMOC = [[[NSManagedObjectContext alloc] init] autorelease];
    
    [self.backgroundMOC setPersistentStoreCoordinator:[self.managedObjectContext persistentStoreCoordinator]];
    

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

    感谢您的帮助,彼得。感谢 Fred McCann 的宝贵博文!

    【讨论】:

    • 好的,经过大量测试,我可以确认这绝对解决了我的问题。只要允许,我就会将此标记为已接受的答案...
    • 感谢您的解决方案!该线程有一个很好的锁定/解锁上下文实现,以避免合并期间的冲突:stackoverflow.com/questions/2009399/…
    • +1 非常感谢您提出问题、解决方案并提供 Fred McCann 博客文章的链接。它对我帮助很大!!!
    • each moc must be instantiated in the thread that will be using it 我虽然只对 MOC 的操作应该在同一个线程上,但是也创建了 MOC 本身,如果这是一个私有 MOC,相关队列还不存在..
    • @János 我也有同样的问题。如何在将要使用它的线程中实例化上下文?该线程尚不存在。我正在使用 Swift,但我不明白“在 _importData 方法中移动”是什么意思。
    猜你喜欢
    • 2011-03-17
    • 2014-02-15
    • 2017-11-22
    • 2013-01-29
    • 2012-07-30
    • 2014-07-04
    • 2023-03-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多