Kir*_*mov 6 deadlock core-data ios magicalrecord ios9
更新:我准备了重现问题的样本,没有神奇的记录.请使用以下URL下载测试项目:https://www.dsr-company.com/fm.php?Download = 1& FileToDL = DeadLockTest_CoreDataWithoutMR.zip
提供的项目有以下问题:从主线程调用的performBlockAndWait中的fetch死锁.
如果使用XCode版本> 6.4编译代码,则会重现该问题.如果使用xCode == 6.4编译代码,则不会重现该问题.
老问题是:
我正致力于IOS移动应用程序的支持.在最近将Xcode IDE从版本6.4更新到版本7.0(支持IOS 9)之后,我遇到了一个关键问题 - 应用程序挂起.使用xCode 6.4的相同构建的应用程序(由相同的源生成)可以正常工作.因此,如果应用程序是使用xCode> 6.4构建的 - 应用程序在某些情况下会挂起.如果应用程序是使用xCode 6.4构建的 - 应用程序工作正常.
我花了一些时间研究这个问题,结果我准备了测试应用程序和类似的情况,就像在我的应用程序中重现问题一样.测试应用程序挂起在Xcode> = 7.0上,但在Xcode 6.4上正常工作
下载测试源的链接:https: //www.sendspace.com/file/r07cln
测试应用程序的要求是:1.必须在系统中安装cocoa pods manager 2.版本2.2的MagicalRecord框架.
测试应用程序按以下方式工作:1.在应用程序启动时,它创建具有10000个简单实体记录的测试数据库,并将它们保存到持久存储.2.在方法viewWillAppear中的应用程序的第一个屏幕上:它运行导致死锁的测试.使用以下算法:
-(NSArray *) entityWithId: (int) entityId inContext:(NSManagedObjectContext *)localContext
{
NSArray * results = [TestEntity MR_findByAttribute:@"id" withValue:[ NSNumber numberWithInt: entityId ] inContext:localContext];
return results;
}
…..
int entityId = 88;
NSManagedObjectContext *childContext1 = [NSManagedObjectContext MR_context];
childContext1.name = @"childContext1";
NSManagedObjectContext *childContext2 = [NSManagedObjectContext MR_context];
childContext2.name = @"childContext2";
NSArray *results = [self entityWithId:entityId inContext: childContext2];
for(TestEntity *d in results)
{
NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); /// this line is the reason of the hangup
}
dispatch_async(dispatch_get_main_queue(), ^
{
int entityId2 = 11;
NSPredicate *predicate2 = [NSPredicate predicateWithFormat:@"id=%d", entityId2];
NSArray *a = [ TestEntity MR_findAllWithPredicate: predicate2 inContext: childContext2];
for(TestEntity *d in a)
{
NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name);
}
});
Run Code Online (Sandbox Code Playgroud)
使用并发类型== NSPrivateQueueConcurrencyType创建两个托管对象上下文(请检查魔法记录框架的MR_context代码).两个上下文都具有并发类型= NSMainQueueConcurrencyType的父上下文.从主线程应用程序以同步方式执行提取(MR_findByAttribute和MR_findAllWithPredicate使用performBlockAndWait,其中包含提取请求).在第一次获取之后,使用dispatch_async()在主线程上调度第二次获取.
结果应用程序挂断了.似乎发生了死锁,请检查堆栈的屏幕截图:
这里是链接,我的声望太低,无法发布图片.https://cdn.img42.com/34a8869bd8a5587222f9903e50b762f9.png)
如果要注释行
NSLog(来自fetchRequest%@的@"e,名称='%@'",d,d.name); ///这一行是挂断的原因
(这是测试项目的ViewController.m中的第39行)应用程序变得正常工作.我相信这是因为没有读取测试实体的名称字段.
所以使用注释行NSLog(@"e来自fetchRequest%@,名称='%@'",d,d.name);
使用Xcode 6.4和Xcode 7.0构建的二进制文件没有挂起.
使用未注释的行NSLog(来自fetchRequest%@的@"e,名称='%@'",d,d.name);
在使用Xcode 7.0构建的二进制文件上有挂起,并且在使用Xcode 6.4构建的二进制文件上没有挂起.
我认为问题是由于实体数据的延迟加载而发生的.
所述案件有问题吗?我将不胜感激任何帮助.
这就是为什么我不使用抽象(即隐藏)太多核心数据细节的框架.它具有非常复杂的使用模式,有时您需要了解它们如何互操作的细节.
首先,我对魔法记录一无所知,只是很多人都使用它,所以它必须非常擅长它的作用.
但是,我立即在您的示例中看到了核心数据并发的几个完全错误的使用,所以我去查看头文件以了解为什么您的代码做出了假设.
我根本不是要打击你,虽然这看起来似乎乍一看.我想帮助教育你(我用这个机会来看看MR).
从一个非常快速的MR看,我会说你对MR的作用有一些误解,还有核心数据的一般并发规则.
首先,你说这个......
使用并发类型== NSPrivateQueueConcurrencyType创建两个托管对象上下文(请检查魔法记录框架的MR_context代码).两个上下文都具有并发类型= NSMainQueueConcurrencyType的父上下文.
这似乎不是真的.这两个新的上下文实际上是私有队列上下文,但是它们的父(根据我在github上看到的代码)是神奇的MR_rootSavingContext,它本身也是一个私有队列上下文.
让我们分解您的代码示例.
NSManagedObjectContext *childContext1 = [NSManagedObjectContext MR_context];
childContext1.name = @"childContext1";
NSManagedObjectContext *childContext2 = [NSManagedObjectContext MR_context];
childContext2.name = @"childContext2";
Run Code Online (Sandbox Code Playgroud)
所以,你现在有两个私有队列MOC(childContext1和childContext2),它们是另一个匿名私有队列MOC的子节点(我们将调用savingContext).
NSArray *results = [self entityWithId:entityId inContext: childContext2];
Run Code Online (Sandbox Code Playgroud)
然后执行提取childContext1.那段代码实际上......
-(NSArray *) entityWithId:(int)entityId
inContext:(NSManagedObjectContext *)localContext
{
NSArray * results = [TestEntity MR_findByAttribute:@"id"
withValue:[NSNumber numberWithInt:entityId]
inContext:localContext];
return results;
}
Run Code Online (Sandbox Code Playgroud)
现在,我们知道localContext在这种情况下,此方法中的另一个指针childContext2是私有队列MOC.在调用之外访问私有队列MOC对并发规则是100%performBlock.但是,由于您正在使用其他API,并且方法名称无法帮助了解如何访问MOC,因此我们需要查看该API并查看它是否隐藏performBlock以查看您是否正确访问它.
遗憾的是,头文件中的文档没有提供任何指示,因此我们必须查看实现.该调用最终调用MR_executeFetchRequest...,这在文档中没有表明它如何处理并发.那么,我们来看看它的实现.
现在,我们正在某个地方.此函数确实尝试安全地访问MOC,但它使用的performBlockAndWait将在调用时阻塞.
这是一个非常重要的信息,因为从错误的地方调用它确实会导致死锁.因此,您必须敏锐地意识到,performBlockAndWait只要您执行获取请求,就会调用它.我个人的规则是永远不要使用,performBlockAndWait除非绝对没有其他选择.
但是,这里的调用应该是完全安全的...假设它不是在父MOC的上下文中调用的.
那么,让我们看看下一段代码.
for(TestEntity *d in results)
{
NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); /// this line is the reason of the hangup
}
Run Code Online (Sandbox Code Playgroud)
现在,这不是MagicalRecord的错,因为MR甚至没有直接在这里使用.但是,您已经接受过使用这些MR_方法的培训,这些方法不需要了解并发模型,因此您要么忘记也要永远不会学习并发规则.
中对象results数组是生活中的所有管理对象childContext2私有队列上下文.因此,如果不向并发规则致敬,您可能永远不会访问它们.这明显违反了并发规则.在开发应用程序时,应该使用参数-com.apple.CoreData.ConcurrencyDebug 1启用并发调试.
此代码段必须包含在performBlock或中performBlockAndWait.我几乎没有用过performBlockAndWait任何东西,因为它有很多缺点 - 死锁就是其中之一.事实上,只是看到使用performBlockAndWait是一个非常强烈的迹象表明你的死锁发生在那里,而不是你指出的代码行.但是,在这种情况下,它至少和之前的fetch一样安全,所以让它更安全一些......
[childContext2 performBlockAndWait:^{
for (TestEntity *d in results) {
NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name);
}
}];
Run Code Online (Sandbox Code Playgroud)
接下来,您将调度到主线程.这是因为你只是想在随后的事件循环周期中发生某些事情,还是因为这段代码已经在其他某个线程上运行了?谁知道.但是,你在这里遇到了同样的问题(为了便于阅读,我重新格式化了你的代码).
dispatch_async(dispatch_get_main_queue(), ^{
int entityId2 = 11;
NSPredicate *predicate2 = [NSPredicate predicateWithFormat:@"id=%d", entityId2];
NSArray *a = [TestEntity MR_findAllWithPredicate:predicate2
inContext:childContext2];
for (TestEntity *d in a) {
NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name);
}
});
Run Code Online (Sandbox Code Playgroud)
现在,我们知道代码开始在主线程上运行,搜索将调用performBlockAndWait但是你在for循环中的后续访问再次违反了核心数据并发规则.
基于此,我看到的唯一真正的问题是......
MR似乎尊重其API中的核心数据并发规则,但在访问托管对象时仍必须遵循核心数据并发规则.
我真的不喜欢使用performBlockAndWait它,因为它只是一个等待发生的问题.
现在,让我们来看看你的挂片截图.嗯...这是一个经典的死锁,但它没有任何意义,因为死锁发生在主线程和MOC线程之间.只有当主队列MOC是此私有队列MOC的父节点时才会发生这种情况,但代码显示情况并非如此.
嗯...它没有意义,所以我下载了你的项目,并查看了你上传的pod中的源代码.现在,该版本的代码使用了MR_defaultContext作为创建的所有MOC的父代MR_context.所以,默认的MOC确实是一个主队列MOC,现在它们都非常有意义.
您有一个MOC作为主队列MOC的子级.当您将该块分派给主队列时,它现在作为主队列上的块运行.然后代码调用performBlockAndWait一个上下文,该上下文是该队列的MOC的子代,这是一个巨大的禁忌,并且几乎可以确保您获得死锁.
因此,似乎MR已经将其代码从使用主队列作为新上下文的父级改为使用私有队列作为新上下文的父级(很可能是由于这个确切的问题).所以,如果你升级到最新版本的MR,你应该没问题.
但是,我仍然会警告你,如果你想以多线程方式使用MR,你必须确切地知道它们如何处理并发规则,并且你必须确保在访问任何非核心数据对象时都遵守它们.通过MR API.
最后,我只是说我已经完成了大量的核心数据,而且我从未使用过试图隐藏并发问题的API.原因是有太多小角落的情况,我宁愿只是以务实的方式处理它们.
最后,你几乎不应该使用,performBlockAndWait除非你确切知道为什么它是唯一的选择.将它作为API下面的一部分使用,至少对我来说更加可怕.
我希望这个小小的短途旅行能够启发并帮助你(以及其他一些人).它确实为我带来了一些亮点,并帮助重建了我之前毫无根据的一些怯懦.
这是对您提供的"非魔法记录"示例的回应.
这段代码的问题与我上面描述的完全相同的问题相对于MR发生的问题.
您有一个私有队列上下文,作为主队列上下文的子级.
您正在主队列上运行代码,并调用performBlockAndWait子上下文,该子上下文在尝试执行提取时必须锁定其父上下文.
它被称为僵局,但更具描述性(和诱人性)的术语是致命的拥抱.
原始代码在主线程上运行.它调用一个子上下文来做某事,并且在孩子完成之前它什么都不做.
那个孩子,为了完成,需要主线程来做某事.但是,主要线程在孩子完成之前无法做任何事情......但是孩子正在等待主线程做某事......
没有人能取得任何进展.
您所面临的问题已有详细记录,实际上,在WWDC演示文稿和多个文档中已经多次提到过.
你永远不应该打电话performBlockAndWait给孩子.
你过去侥幸逃脱的事实只是一个"偶然事件",因为它根本不应该以那种方式工作.
实际上,你几乎不应该每次通话performBlockAndWait.
你应该真的习惯于进行异步编程.以下是我建议您重写此测试的方法,以及提示此问题的任何内容.
首先,重写fetch,使其异步工作......
- (void)executeFetchRequest:(NSFetchRequest *)request
inContext:(NSManagedObjectContext *)context
completion:(void(^)(NSArray *results, NSError *error))completion
{
[context performBlock:^{
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
if (completion) {
completion(results, error);
}
}];
}
Run Code Online (Sandbox Code Playgroud)
然后,你改变调用fetch的代码来做这样的事情......
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity: testEntityDescription ];
[request setPredicate: predicate2 ];
[self executeFetchRequest:request
inContext:childContext2
completion:^(NSArray *results, NSError *error) {
if (results) {
for (TestEntity *d in results) {
NSLog(@"++++++++++ e from fetchRequest %@ with name = '%@'", d, d.name);
}
} else {
NSLog(@"Handle this error: %@", error);
}
}];
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1211 次 |
| 最近记录: |