【问题标题】:xCode 7.0 IOS9 SDK: deadlock while executing fetch request with performBlockAndWaitxCode 7.0 IOS9 SDK:使用 performBlockAndWait 执行获取请求时出现死锁
【发布时间】:2015-12-29 12:39:55
【问题描述】:

更新:我已经准备了没有神奇记录的重现问题的示例。请使用以下 URL 下载测试项目: https://www.dsr-company.com/fm.php?Download=1&FileToDL=DeadLockTest_CoreDataWithoutMR.zip

提供的项目有以下问题:获取时出现死锁 在从主线程调用的 performBlockAndWait 中。

如果使用 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);
                   }
               });

使用并发类型== NSPrivateQueueConcurrencyType 创建了两个托管对象上下文(请查看魔法记录框架的MR_context 的代码)。两个上下文都有父上下文 并发类型 = NSMainQueueConcurrencyType。从主线程应用程序以同步方式执行获取( MR_findByAttribute 和 MR_findAllWithPredicate 与内部获取请求一起使用 performBlockAndWait )。在第一次 fetch 之后,第二次 fetch 使用 dispatch_async() 在主线程上调度。

因此应用程序挂起。好像发生了死锁,请查看堆栈截图:

这是链接,我的声誉太低,无法发布图像。 https://cdn.img42.com/34a8869bd8a5587222f9903e50b762f9.png)

如果要评论该行
NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); /// 这行是挂断的原因

(这是测试项目的 ViewController.m 中的第 39 行)应用程序运行正常。我相信这是因为没有读取测试实体的名称字段。

所以用注释行 NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name);
使用 Xcode 6.4 和 Xcode 7.0 构建的二进制文件没有任何问题。

带有未注释的行 NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name);

使用 Xcode 7.0 构建的二进制文件有挂断,使用 Xcode 6.4 构建的二进制文件没有挂断。

我认为问题是由于实体数据的延迟加载而发生的。

对所描述的案例有任何问题吗?如有任何帮助,我将不胜感激。

【问题讨论】:

  • 我认为最近的 iOS 9 SDK 存在问题或不兼容,但为了帮助人们解决这个问题,让我们让您的帖子更加清晰。首先,你能把你的例子上传到github吗? (Chrome 不允许我下载)。第二个:删除您对 pod 所做的所有更改。删除未使用的代码,使示例清晰。问题的简短故事是:如果 1)从 -viewWillAppear 分派,并且 2)在分派之前使用了托管对象(触发错误)(就像您在 NSLog 中所做的那样:d.name ) 直接CoreDa也有同样的问题

标签: ios core-data deadlock ios9 magicalrecord


【解决方案1】:

我们切换到 XCode7,我刚刚遇到了类似的死锁问题,performBlockAndWait 在 XCode6 中运行良好的代码中。

问题似乎是上游使用dispatch_async(mainQueue, ^{ ... 将网络操作的结果传回。在我们为 CoreData 添加并发支持后,不再需要该调用,但不知何故,它被留下了,直到现在似乎从未引起问题。

Apple 可能会在幕后更改某些内容以使潜在的死锁更加明确。

【讨论】:

  • 我已将问题报告给 Apple 支持,他们说这是错误。我已经向他们发送了修改后的资源,这些资源在没有神奇的记录框架的情况下重现了问题。所以我们应该等到Apple修复它。顺便说一句,他们说这个问题可以使用 NSManagedContext 的 reset() 方法解决
【解决方案2】:

这就是为什么我不使用抽象(即隐藏)核心数据太多细节的框架。它的使用模式非常复杂,有时您需要了解它们如何互操作的细节。

首先,我对魔法唱片一无所知,只是很多人都在使用它,所以它的作用一定很不错。

但是,我立即在您的示例中看到了几个完全错误的核心数据并发用法,因此我查看了头文件以了解您的代码为何做出这样的假设。

我根本不是要抨击你,尽管乍一看这似乎是这样。我想帮助教育你(我借此机会来看看 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";

因此,您现在有两个私有队列 MOC(childContext1childContext2),它们都是另一个匿名私有队列 MOC(我们将调用 savingContext)的子级。

NSArray *results = [self entityWithId:entityId inContext: childContext2];

然后您对childContext1 执行提取。那个代码其实是……

-(NSArray *) entityWithId:(int)entityId
                inContext:(NSManagedObjectContext *)localContext 
{
   NSArray * results = [TestEntity MR_findByAttribute:@"id"
                                            withValue:[NSNumber numberWithInt:entityId]
                                            inContext:localContext];
  return results;
}

现在,我们知道这个方法中的localContext 在这种情况下是另一个指向childContext2 的指针,它是一个私有队列MOC。在调用 performBlock 之外访问私有队列 MOC 是 100% 违反并发规则的。但是,由于您使用的是另一个 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
}

现在,这不是 MagicalRecord 的错,因为这里甚至没有直接使用 MR。但是,您已经接受过使用那些 MR_ 方法的培训,这些方法不需要了解并发模型,因此您要么忘记要么永远不会学习并发规则。

results 数组中的对象都是位于 childContext2 私有队列上下文中的托管对象。因此,如果不尊重并发规则,您可能永远无法访问它们。这明显违反了并发规则。在开发应用程序时,您应该使用参数 -com.apple.CoreData.ConcurrencyDebug 1 启用并发调试。

此代码 sn-p 必须包含在 performBlockperformBlockAndWait 中。我几乎从不使用performBlockAndWait 做任何事情,因为它有很多缺点——死锁就是其中之一。事实上,仅仅看到performBlockAndWait 的使用就非常强烈地表明您的死锁正在那里发生,而不是在您指示的代码行上。不过,在这种情况下,它至少和之前的 fetch 一样安全,所以让我们让它更安全一点……

[childContext2 performBlockAndWait:^{
    for (TestEntity *d in results) {
        NSLog(@"e from fetchRequest %@ with name = '%@'", d,  d.name);
    }
}];

接下来,您分派到主线程。那是因为您只是希望在随后的事件循环周期中发生某些事情,还是因为该代码已经在其他某个线程上运行?谁知道。但是,您在这里遇到了同样的问题(为了便于阅读,我将您的代码重新格式化为帖子)。

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);
    }
});

现在,我们知道代码开始在主线程上运行,搜索将调用performBlockAndWait,但您在 for 循环中的后续访问再次违反了核心数据并发规则。

基于此,我看到的唯一真正的问题是......

  1. MR 似乎遵守其 API 中的核心数据并发规则,但在访问托管对象时仍必须遵循核心数据并发规则。

  2. 我真的不喜欢使用performBlockAndWait,因为它只是一个等待发生的问题。

现在,让我们看一下你挂起的屏幕截图。嗯...这是一个经典的死锁,但它没有任何意义,因为死锁发生在主线程和 MOC 线程之间。只有当主队列 MOC 是这个私有队列 MOC 的父级时才会发生这种情况,但代码显示情况并非如此。

嗯...这没有意义,所以我下载了您的项目,并查看了您上传的 pod 中的源代码。现在,该版本的代码使用MR_defaultContext 作为使用MR_context 创建的所有MOC 的父代。所以,默认的 MOC 确实是一个主队列 MOC,现在这一切都说得通了。

您有一个 MOC 作为主队列 MOC 的子节点。当您将该块分派到主队列时,它现在作为主队列上的一个块运行。然后代码在作为该队列的 MOC 的子上下文的上下文中调用 performBlockAndWait,这是一个巨大的禁忌,并且您几乎肯定会遇到死锁。

因此,似乎 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);
        }
    }];
}

然后,您更改调用 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);
    }
}];

【讨论】:

  • 这里是没有神奇记录的重现问题的示例的 URL。 dsr-company.com/… 提供的项目存在以下问题:从主线程调用的 performBlockAndWait 中获取死锁。如果使用 XCode 版本 > 6.4 编译代码,则会重现该问题。如果使用 xCode == 6.4 编译代码,则不会重现该问题。有人有什么想法吗?
  • @KirillNeznamov - 该代码所做的与我描述的您使用的 MR 版本所做的完全相同。有关详细信息,请参阅编辑。
  • 谢谢乔迪,我会尝试在苹果官方文档中找到关于答案的注释。非常感谢!
  • 出色的分析!关于“我看到的唯一真正的问题是......”中的两点完全同意,但 FWIW 我会添加另一个:默认情况下,库应该将客户端提供的块(或闭包)分派到私有队列,它应该也不要将队列或线程的默认值设置到主线程 - 除非客户端明确设置。
  • @CouchDeveloper - 我同意。请随时编辑答案以添加您在这方面的想法。
猜你喜欢
  • 2014-03-29
  • 1970-01-01
  • 2012-09-12
  • 2016-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-09
相关资源
最近更新 更多