CoreData 多线程死锁
Posted
技术标签:
【中文标题】CoreData 多线程死锁【英文标题】:CoreData deadlock with multiple threads 【发布时间】:2013-04-16 15:30:45 【问题描述】:我遇到了在多 NSManagedObjectContexts 和多线程场景中发生的相同死锁问题(这在 SO 上很常见)。在我的一些视图控制器中,我的应用程序使用后台线程从 Web 服务获取数据,并在同一个线程中保存它。在其他情况下,如果不保存就不会继续前进(例如,当他们点击“下一步”时从表单中保留值),保存是在主线程上完成的。 AFAIK理论上应该没有错,但偶尔我可以在调用
时发生死锁if (![moc save:&error])
...当死锁发生时,这似乎总是在后台线程的保存中。并非每次通话都会发生这种情况。事实上恰恰相反,我必须使用我的应用程序几分钟,然后它就会发生。
我已经阅读了我能找到的所有帖子以及 Apple 文档等,我确信我会遵循这些建议。具体来说,我对使用多个 MOC/线程的理解归结为:
每个线程都必须有自己的 MOC。 必须在该线程上创建线程的 MOC(而不是从一个线程传递到另一个线程)。 不能传递 NSManagedObject,但可以传递 NSManagedObjectID,并且您可以使用 ID 使用不同的 MOC 来扩充 NSManagedObject。 如果它们都使用相同的 PersistentStoreCoordinator,则必须将来自一个 MOC 的更改合并到另一个 MOC。不久前,我在this SO thread 上遇到了一些 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
这是一个似乎死锁的多线程位的 sn-p:
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 - 所以我认为它实际上并没有在线程上死锁。 Here's a screehshot 暂停时两个线程的状态。
【问题讨论】:
当你的应用死锁时,它是通过什么方法挂在主线程上的? @lassej 这是 Thread1 的打印输出: libsystem_kernel.dylib`mach_msg_trap: 0x98030c18: movl $4294967265, %eax 0x98030c1d: calll 0x9803449a ; _sysenter_trap 0x98030c22: ret 0x98030c23: nop 我不确定这就是您所需要的。 Thread6 在 [NSManagedObjectContext 保存]。我不确定 Thread1 是否真的被阻塞了,但如果我理解正确的话,这是两个线程的 MOC 的死锁。 如果您使用调试器从 xcode 启动应用程序并在死锁后单击暂停按钮,您将看到如下调用堆栈:callstack screenshot。 查看上面的编辑帖子 【参考方案1】:我完全不推荐你的方法。首先,除非你局限于 ios4,否则你应该使用 MOC 并发类型,而不是旧方法。即使在 iOS 5 下(嵌套上下文已被破坏),performBlock
方法也更加合理。
另外,请注意dispatch_get_global_queue
提供了一个并发队列,不能用于同步。
详情见这里:http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/CoreData/Articles/cdConcurrency.html
编辑
您正在尝试手动管理 MOC 和线程。如果你愿意,你可以做到,但你的道路上有龙。这就是创建新方法的原因,以最大程度地减少跨多个线程使用 Core Data 时出错的机会。每当我看到使用 Core Data 进行手动线程管理时,我总是建议将更改作为第一种方法。这将立即消除大多数错误。
除了您手动映射 MOC 和线程之外,我不需要看到更多内容,就知道您是在自找麻烦。只需重新阅读该文档,并以正确的方式进行操作(使用 performBlock
)。
【讨论】:
谢谢@Jody Hagins,虽然我不清楚你所说的细节。我已经阅读了该链接,并且 AFAIK 它与我正在做的事情非常一致(所以我显然遗漏了一些东西) - 但我正在做的是“旧方法”吗?与 MOC 并发类型有什么区别? MOC 并发类型是什么? 好的,抱歉,我了解 MOC 并发类型,但我不明白为什么这仍然无法正常工作。看,关于你关于全局队列和同步的观点,我的应用程序从来没有同时在多个线程上运行数据库操作,所以我认为这不应该是一个问题,不是吗?我真的只使用 GCD,所以我可以在进行长时间的 Web 服务点击时更新 UI(例如进度条)。 谢谢,一分钱开始下降...但问题是我需要使用多线程(在我的情况下通过 GCD)出于不同的原因 - 主要是长期运行的 Web 服务调用,那么 performBlock 和 performBlockAndWait 如何适应呢?鉴于这些是 MOC 上的方法,我可以将我的 web 服务调用放在 performBlock 中,然后立即将结果数据保留在块中吗?我的 UI 更新依赖于这些数据更改会怎样?是否有一个外部 nonMainMOC performBlock 在外部有一个 mainMOC performBlock 的情况? 我完全重新调整了我的核心数据代码以使用 performBlock 和 performBlockAndWait。看起来很好而且更简单。感谢大家的帮助。 您介意分享您修改后的代码吗?我似乎有类似的问题,我猜这个问题来自我对 CoreData 缺乏了解!以上是关于CoreData 多线程死锁的主要内容,如果未能解决你的问题,请参考以下文章