我遇到了在多 NSManagedObjectContexts 和多线程场景中发生的相同死锁问题(这在 SO 上很常见)。在我的一些视图控制器中,我的应用程序使用后台线程从 Web 服务获取数据,并在同一个线程中保存它。在其他情况下,如果不保存就不会继续前进(例如,当他们点击“下一步”时从表单中保留值),保存是在主线程上完成的。AFAIK理论上应该没有错,但有时我可以在调用
if (![moc save:&error])
...当死锁发生时,这似乎总是在后台线程的保存中。并非每次通话都会发生这种情况。事实上恰恰相反,我必须使用我的应用程序几分钟,然后它就会发生。
我已经阅读了我能找到的所有帖子以及 Apple 文档等,我确信我正在遵循这些建议。具体来说,我对使用多个 MOC/线程的理解归结为:
- 每个线程必须有自己的 MOC。
- 必须在该线程上创建线程的 MOC(而不是从一个线程传递到另一个线程)。
- 一个 NSManagedObject 不能被传递,但是一个 NSManagedObjectID 可以,并且你使用这个 ID 来使用不同的 MOC 来膨胀一个 NSManagedObject。
- 如果它们都使用相同的 PersistentStoreCoordinator,则必须将来自一个 MOC 的更改合并到另一个 MOC。
不久前,我在这个 SO 线程上遇到了一些 MOC 帮助程序类的代码,发现它很容易理解并且使用起来非常方便,所以我现在所有的 MOC 交互都是通过它进行的。这是我的 ManagedObjectContextHelper 类的全部内容:
#import "ManagedObjectContextHelper.h"
@implementation ManagedObjectContextHelper
+(void)initialize {
[[NSNotificationCenter defaultCenter] addObserver:[self class]
selector:@selector(threadExit:)
name:NSThreadWillExitNotification
object:nil];
}
+(void)threadExit:(NSNotification *)aNotification {
TDAppDelegate *delegate = (TDAppDelegate *)[[UIApplication sharedApplication] delegate];
NSString *threadKey = [NSString stringWithFormat:@"%p", [NSThread currentThread]];
NSMutableDictionary *managedObjectContexts = delegate.managedObjectContexts;
[managedObjectContexts removeObjectForKey:threadKey];
}
+(NSManagedObjectContext *)managedObjectContext {
TDAppDelegate *delegate = (TDAppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *moc = delegate.managedObjectContext;
NSThread *thread = [NSThread currentThread];
if ([thread isMainThread]) {
[moc setMergePolicy:NSErrorMergePolicy];
return moc;
}
// a key to cache the context for the given thread
NSString *threadKey = [NSString stringWithFormat:@"%p", thread];
// delegate.managedObjectContexts is a mutable dictionary in the app delegate
NSMutableDictionary *managedObjectContexts = delegate.managedObjectContexts;
if ( [managedObjectContexts objectForKey:threadKey] == nil ) {
// create a context for this thread
NSManagedObjectContext *threadContext = [[NSManagedObjectContext alloc] init];
[threadContext setPersistentStoreCoordinator:[moc persistentStoreCoordinator]];
[threadContext setMergePolicy:NSErrorMergePolicy];
// cache the context for this thread
NSLog(@"Adding a new thread:%@", threadKey);
[managedObjectContexts setObject:threadContext forKey:threadKey];
}
return [managedObjectContexts objectForKey:threadKey];
}
+(void)commit {
// get the moc for this thread
NSManagedObjectContext *moc = [self managedObjectContext];
NSThread *thread = [NSThread currentThread];
if ([thread isMainThread] == NO) {
// only observe notifications other than the main thread
[[NSNotificationCenter defaultCenter] addObserver:[self class] selector:@selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:moc];
}
NSError *error;
if (![moc save:&error]) {
NSLog(@"Failure is happening on %@ thread",[thread isMainThread]?@"main":@"other");
NSArray* detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey];
if(detailedErrors != nil && [detailedErrors count] > 0) {
for(NSError* detailedError in detailedErrors) {
NSLog(@" DetailedError: %@", [detailedError userInfo]);
}
}
NSLog(@" %@", [error userInfo]);
}
if ([thread isMainThread] == NO) {
[[NSNotificationCenter defaultCenter] removeObserver:[self class] name:NSManagedObjectContextDidSaveNotification
object:moc];
}
}
+(void)contextDidSave:(NSNotification*)saveNotification {
TDAppDelegate *delegate = (TDAppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *moc = delegate.managedObjectContext;
[moc performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:)
withObject:saveNotification
waitUntilDone:NO];
}
@end
这是似乎死锁的多线程位的片段:
NSManagedObjectID *parentObjectID = [parent objectID];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
// GET BACKGROUND MOC
NSManagedObjectContext *backgroundContext = [ManagedObjectContextHelper managedObjectContext];
Parent *backgroundParent = (Parent*)[backgroundContext objectWithID:parentObjectID];
// HIT THE WEBSERVICE AND PUT THE RESULTS IN THE PARENT OBJECT AND ITS CHILDREN, THEN SAVE...
[ManagedObjectContextHelper commit];
dispatch_sync(dispatch_get_main_queue(), ^{
NSManagedObjectContext *mainManagedObjectContext = [ManagedObjectContextHelper managedObjectContext];
parent = (Parent*)[mainManagedObjectContext objectWithID:parentObjectID];
});
});
错误中的 conflictList 似乎表明它与父对象的 ObjectID 有关:
conflictList = (
"NSMergeConflict (0x856b130) for NSManagedObject (0x93a60e0) with objectID '0xb07a6c0 <x-coredata://B7371EA1-2532-4D2B-8F3A-E09B56CC04F3/Child/p4>'
with oldVersion = 21 and newVersion = 22
and old object snapshot = {\n parent = \"0xb192280 <x-coredata://B7371EA1-2532-4D2B-8F3A-E09B56CC04F3/Parent/p3>\";\n name = \"New Child\";\n returnedChildId = 337046373;\n time = 38;\n}
and new cached row = {\n parent = \"0x856b000 <x-coredata://B7371EA1-2532-4D2B-8F3A-E09B56CC04F3/Parent/p3>\";\n name = \"New Child\";\n returnedChildId = 337046373;\n time = 38;\n}"
);
我已经尝试在获得 MOC 后立即调用 refreshObject,其理论是如果这是我们之前使用过的 MOC(例如,我们之前在主线程上使用过 MOC,很可能这与助手类将为我们提供的相同),那么也许在另一个线程中保存意味着我们需要显式刷新。但这没有任何区别,如果我继续点击足够长的时间,它仍然会死锁。
有没有人有任何想法?
编辑:如果我为所有异常设置了断点,那么调试器会自动暂停if (![moc save:&error])
在线,因此播放/暂停按钮已经暂停并显示播放三角形。如果我禁用所有异常的断点,那么它实际上会记录冲突并继续 - 可能是因为合并策略当前设置为 NSErrorMergePolicy - 所以我认为它实际上并没有在线程上死锁。这是暂停时两个线程状态的屏幕截图。