xCode 7.0 IOS9 SDK:使用performBlockAndWait

时间:2015-10-01 12:43:29

标签: ios core-data deadlock ios9 magicalrecord

更新:我已准备好样本,无需魔法记录即可重现问题。请使用以下URL下载测试项目: https://www.dsr-company.com/fm.php?Download=1&FileToDL=DeadLockTest_CoreDataWithoutMR.zip

提供的项目存在以下问题:fetch死锁 在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和里面的获取请求)。在第一次获取之后,使用dispatch_async()在主线程上调度第二次获取。

结果应用程序挂断了。似乎发生了死锁,请检查堆栈的屏幕截图:

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

如果要对该行发表评论   NSLog(@“来自fetchRequest%@,名称='%@'”,d,d.name); ///这一行是挂断的原因

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

所以使用注释行 NSLog(@“来自fetchRequest%@,名称='%@'”,d,d.name);
使用Xcode 6.4和Xcode 7.0构建的二进制文件没有挂断。

使用未注释的行 NSLog(@“来自fetchRequest%@,名称='%@'”,d,d.name);

在使用Xcode 7.0构建的二进制文件上有挂起,并且在使用Xcode 6.4构建的二进制文件上没有挂起。

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

所描述的案件有人有问题吗?我将不胜感激任何帮助。

2 个答案:

答案 0 :(得分:8)

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

首先,我对魔法记录一无所知,只是很多人都使用它,所以它必须非常擅长它的作用。

但是,我立即在示例中看到了核心数据并发的几个完全错误的使用,所以我去查看头文件,看看为什么你的代码做了假设。

我根本不打算抨击你,虽然这看起来好像乍一看。我想帮助教育你(我用这个机会来看看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启用并发调试。

此代码段必须包含在performBlockperformBlockAndWait中。我几乎没有使用performBlockAndWait来做任何事情,因为它有很多缺点 - 死锁就是其中之一。事实上,只是看到performBlockAndWait的使用是一个非常强烈的迹象,表明你的死锁正在那里发生,而不是你指出的代码行。但是,在这种情况下,它至少和之前的提取一样安全,所以让它更安全......

[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,因为这只是一个等待发生的问题。

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

    嗯...它没有意义,所以我下载了你的项目,并查看了你上传的pod中的源代码。现在,该版本的代码使用MR_defaultContext作为使用MR_context创建的所有MOC的父代。所以,默认的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);
            }
        }];
    }
    

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

答案 1 :(得分:1)

我们切换到XCode7,我在代码中遇到了与performBlockAndWait类似的死锁问题,在XCode6中工作正常。

问题似乎是上游使用dispatch_async(mainQueue, ^{ ...来传回网络操作的结果。在我们为CoreData添加并发支持后不再需要该调用,但不知怎的,它已经离开,直到现在才出现问题。

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