Core Data私有队列performBlockAndWait访问关系时出现死锁

12

这个话题已经在许多论坛上讨论过了,但是我仍然不能完全理解 performBlockAndWait 是如何工作的。根据我的理解,context.performBlockAndWait(block: () -> Void) 会在它自己的队列中执行该块并阻塞调用线程。文档说:

您可以在一个块内部将“标准”消息分组发送到上下文中以传递给这些方法。

什么是“标准”消息? 它还说:

基于队列的托管对象上下文上的Setter方法是线程安全的。您可以直接在任何线程上调用这些方法。

这是否意味着我可以在 performBlock* APIs 的上下文之外设置已获取的托管对象的属性?

据我所知,在具有并发类型 .MainQueueConcurrencyType 的上下文上调用 performBlockAndWait(block: () -> Void) 将在从主线程调用时创建死锁并永久阻塞 UI。但是在我的测试中,它并没有创建任何死锁。

我认为它应该会创建死锁的原因是,performBlockAndWait 首先会阻塞调用线程,然后在它自己的线程上执行块。由于上下文必须执行其块的线程与已经被阻塞的调用线程相同,因此它将永远无法执行其块并且线程将一直保持阻塞状态。

但是我在一些奇怪的场景中遇到了死锁。我有以下测试代码:

@IBAction func fetchAllStudentsOfDepartment(sender: AnyObject) {

    let entity = NSEntityDescription.entityForName("Department", inManagedObjectContext: privateContext)
    let request = NSFetchRequest()
    request.entity = entity
    request.relationshipKeyPathsForPrefetching = ["students"]
    var department: Department?

    privateContext.performBlockAndWait { () -> Void in
        department = try! self.privateContext.executeFetchRequest(request).first as? Department
        print(department?.name)
        guard let students = department?.students?.allObjects as? [Student] else {
            return
        }
        for student in students {
            print(student.firstName)
        }
    }
}

@IBAction func fetchDepartment(sender: AnyObject) {

    let entity = NSEntityDescription.entityForName("Department", inManagedObjectContext: privateContext)
    let request = NSFetchRequest()
    request.entity = entity

    privateContext.performBlockAndWait { () -> Void in
        let department = try! self.privateContext.executeFetchRequest(request).first as? Department
        print(department?.name)

    }

    privateContext.performBlockAndWait { () -> Void in
        let department = try! self.privateContext.executeFetchRequest(request).first as? Department
        print(department?.name)
    }
}

请注意,在我的测试代码的fetchDepartment方法中,我意外地两次粘贴了performBlockAndWait

  • 如果没有调用fetchAllStudentsOfDepartment方法,则不会导致死锁。 但是一旦调用fetchAllStudentsOfDepartment,则对fetchDepartment方法的任何调用都会永久阻塞UI。
  • 如果我删除fetchAllStudentsOfDepartment方法中的print(student.firstName),那么它就不会阻塞。这意味着,仅当访问关系的属性时,它才会阻塞UI。
  • privateContextconcurrencyType设置为.PrivateQueueConcurrencyType。上面的代码仅在privateContextparentContextconcurrencyType设置为.MainQueueConcurrencyType时才会阻塞UI。

    我已经使用其他.xcdatamodel测试了相同的代码,并且现在我确定只有在访问关系的属性时才会阻塞。 我当前的.xcdatamodel如下所示: data model

如果信息有冗余,请谅解,但是我只是分享了在已经花费了大约8个小时后的所有观察结果。当UI被阻止时,我可以发布我的线程堆栈。 总之,我有三个问题:

  1. 什么是“标准”消息?
  2. 我们可以设置在performBlock* API之外的上下文中获取的托管对象的属性吗?
  3. 为什么在我的测试代码中,performBlockAndWait的表现不佳并导致UI阻塞。

测试代码:您可以从此处下载测试代码。

2个回答

8
  1. 标准消息是旧的Objective-C术语。这意味着您应该在performBlockperformBlockAndWait中对ManagedObjectContext及其子ManagedObjects执行所有常规方法调用。除了initsetParentContext之外,在块之外的私有上下文中允许的唯一调用是任何其他内容都应在块中完成。

  2. 不行。任何从私有上下文获取的托管对象都必须仅在该私有上下文的队列上访问(read or write)。从另一个队列访问违反了线程限制规则。

  3. 你遇到阻塞问题的原因是你有两个级别的“mainQueue”上下文,这是“智能化”队列系统的结果。以下是流程:

    • 在主队列上创建一个上下文,然后将其创建为另一个主队列上下文的子项。
    • 创建该第二层主队列上下文的私有子项
    • 以使它正在尝试在主队列上下文上已经加载的对象上触发错误的方式访问该私有队列上下文。

由于存在两个级别的主队列上下文,它会导致死锁,而通常队列系统会看到潜在的死锁并避免它。

您可以通过将mainContext变量更改为以下内容来进行测试:

lazy var mainContext: NSManagedObjectContext = {
    let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate
    return appDelegate!.managedObjectContext
}

如果队列系统看到阻塞并避免它,您的问题就会消失。 通过在performBlockAndWait()中设置断点,您甚至可以看到这种情况发生,并且仍然在主队列上。

最后,在父/子设计中没有理由像那样拥有两个级别的主队列上下文。 如果有什么的话,这是一个不应该这样做的好论据。

更新

我错过了您已更改appDelegate中的模板代码并将整体上下文转换为私有的事实。

每个vc都有一个主MOC的模式会浪费Core Data的许多优势。 虽然在顶部拥有一个私有和一个主MOC(它存在于整个应用程序中,而不仅仅是一个VC)是一种有效的设计,但如果您从主队列这样使用performBlockAndWait,它将无法正常工作。

我不建议从主队列中使用performBlockAndWait,因为您正在阻止整个应用程序。 performBlockAndWait只应在调用主队列(或者可能是一个后台到另一个后台)时使用。


顺便说一下,从主队列调用performBlockAndWait几乎总是一个坏主意。 - Marcus S. Zarra
感谢您的澄清,现在我明白了。但是我不太理解"两个级别的主队列上下文"。只有一个上下文具有MainQueue并发性质。AppDelegate的上下文具有PrivateQueue并发性质。我认为这是推荐的嵌套上下文模式。 持久化存储-> PrivateQueueContext-> MainQueueContext->其他PrivateQueueContext / s。 如果我将mainContext = appDelegate的context,则我的应用程序中将没有使用MainQueue并发类型的上下文。 - Vishal Singh
我按照 https://www.cocoanetics.com/2012/07/multi-context-coredata/ 中“异步保存”部分所描述的模式进行操作。 - Vishal Singh
@MarcusS.Zarra,你能看一下这个相关主题的问题吗?(http://stackoverflow.com/questions/41073296/core-data-concurrency-performblockandwait-nsmanagedobjectcontext-zombie) - Neil Galiaskarov
@NeilGaliaskarov 看起来你已经得到帮助了,有 Mundi 帮助你是很好的。 - Marcus S. Zarra

5
  1. 什么是“标准”消息?

指发送到托管对象上下文或任何托管对象的任何消息。请注意,文档继续澄清...

There are two exceptions:

* Setter methods on queue-based managed object contexts are thread-safe.
  You can invoke these methods directly on any thread.

* If your code is executing on the main thread, you can invoke methods on the
  main queue style contexts directly instead of using the block based API.

因此,在MOC上除了setter方法,其他任何方法都必须从performBlock内部调用。对于NSMainQueueConcurrencyType的MOC,可以在主线程中调用任何方法,而无需将其包装在performBlock中。
2. 我们能否在performBlock* API之外的上下文中设置已获取的托管对象的属性?
不行。任何托管对象的访问都必须受到所在托管对象上下文中performBlock的保护。请注意,如果托管对象位于主队列MOC中并且正在从主队列访问,则存在例外情况。
3. 为什么我的测试代码中performBlockAndWait表现异常并导致UI阻塞?
它没有出现异常。当已处理performBlock[AndWait]调用时,performBlockAndWait是可重入的。
除非没有其他选择,否则永远不要使用performBlockAndWait。特别是在嵌套上下文中,这会带来问题。
请改用performBlock

不应该在看足球比赛(阿肯色/堪萨斯州)时写帖子...我分心了,到半场我才发帖,但是马库斯已经回答了...但我直到发完帖子后才看到。对于重复的帖子感到抱歉。 - Jody Hagins
谢谢。很有道理。我不应该使用performBlockAndWait。但是如果我必须使用,我至少不应该从主线程调用它。 - Vishal Singh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接