【问题标题】:Core Data privateQueue performBlockAndWait deadlock while accessing relationshipCore Data privateQueue performBlockAndWait 在访问关系时出现死锁
【发布时间】:2016-04-06 10:40:27
【问题描述】:

这个话题已经在很多论坛上讨论过,但我仍然无法完全理解performBlockAndWait 的实际工作原理。据我了解,context.performBlockAndWait(block: () -> Void) 将在阻塞调用者线程的同时在自己的队列中执行阻塞。 Documentation 说:

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

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

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

这是否意味着我可以设置在performBlock* API 内部获取的托管对象的属性,该上下文在performBlock* API 之外的上下文中?

根据我的理解,在并发类型为.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 方法的任何调用 永远阻塞用户界面。
  • 如果我在fetchAllStudentsOfDepartment 方法中删除print(student.firstName),那么它不会阻塞。这意味着,只有当我访问关系的属性时,它才会阻止 UI。
  • privateContext 已将 concurrencyType 设置为 .PrivateQueueConcurrencyType。仅当privateContextparentContextconcurrencyType 设置为.MainQueueConcurrencyType 时,上述代码才会阻止UI。

    我也用其他.xcdatamodel 测试了相同的代码,我现在确信它只会在访问关系的属性时阻塞。我当前的.xcdatamodel 看起来像:

如果信息无关紧要,请原谅我,但我只是在花了大约 8 小时后才分享我的所有观察结果。当 UI 被阻塞时,我可以发布我的线程堆栈。总而言之,我有三个问题:

  1. 什么是“标准”消息?
  2. 我们能否设置在performBlock* API 内部获取的托管对象的属性,该 API 在 performBlock* 外部的上下文中?
  3. 为什么 performBlockAndWait 在我的测试代码中行为不端并导致 UI 阻塞。

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

【问题讨论】:

    标签: ios multithreading swift core-data nsmanagedobjectcontext


    【解决方案1】:
    1. 标准消息是旧的 Objective-C 术语。这意味着您应该对 performBlockperformBlockAndWait 中的 ManagedObjectContext 及其子 ManagedObjects 执行所有常规方法调用。在块外的私有上下文中允许的唯一调用是initsetParentContext。其他任何事情都应该在一个块中完成。

    2. 没有。从私有上下文获取的任何托管对象只能在该私有上下文的队列中访问。从另一个队列访问(读取或写入)违反了线程限制规则。

    3. 您遇到阻塞问题的原因是因为您有两个级别的“mainQueue”上下文,那就是“智取”队列系统。这是流程:

      • 您在主队列上创建一个上下文,然后将其创建为另一个主队列上下文的子级。
      • 您创建第二层主队列上下文的私有子节点
      • 您访问该私有队列上下文的方式是它试图在当前已加载到主队列上下文中的对象中出错。

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

    您可以通过将mainContext 变量更改为:

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

    并且您的问题消失了,因为队列系统会看到阻塞并避免它。您甚至可以通过在 performBlockAndWait() 内放置一个断点来查看发生的情况,并看到您仍在主队列中。

    最后,没有理由在父/子设计中拥有两个级别的主队列上下文。如果有的话,这是一个很好的理由,不要这样做。

    更新

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

    每个 vc 都有一个主 MOC 的模式抛弃了 Core Data 的许多好处。虽然顶部有一个私有的 MOC 和一个主 MOC(存在于整个应用程序,而不仅仅是一个 VC)是一种有效的设计,但如果您在主队列中像这样执行performBlockAndWait,它将无法工作。

    我不建议您使用主队列中的performBlockAndWait,因为您会阻塞整个应用程序。 performBlockAndWait 应该只在调用 TO 主队列(或者可能一个背景到另一个背景)时使用。

    【讨论】:

    • 顺便说一句,从主队列调用performBlockAndWait几乎总是一个的想法。
    • 感谢您的澄清。这说得通。但是,我得到了“两个级别的主队列上下文”。只有一个上下文具有 MainQueue 并发。 AppDelegate 的上下文有并发 PrivateQueue。我认为这是推荐的嵌套上下文模式。 PersistenceStore -> PrivateQueueContext -> MainQueueContext -> 其他 PrivateQueueContext/s。如果我使 mainContext = appDelegate 的上下文,那么我的应用程序中将没有 MainQueue 并发类型的上下文。
    • 我遵循“异步保存”部分下cocoanetics.com/2012/07/multi-context-coredata 中描述的模式。
    • @MarcusS.Zarra 你能看看this related topic question 吗?
    • @NeilGaliaskarov 看起来你已经得到了帮助,你与 Mundi 相处得很好。
    【解决方案2】:
    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 内部调用。 MOC 上属于NSMainQueueConcurrencyType 的任何方法都可以从主线程调用,而无需包裹在performBlock 中。

    1. 我们能否设置在 performBlock* 之外的上下文的 performBlock* API 中获取的托管对象的属性?

    没有。对托管对象的任何访问都必须在托管对象所在的托管对象上下文的performBlock 内部进行保护。请注意驻留在从主队列访问的主队列 MOC 中的托管对象的例外情况。

    1. 为什么 performBlockAndWait 行为异常并导致我的测试代码中出现 UI 阻塞。

    这不是行为不端。 performBlockAndWait 是可重入的,但仅在已经处理 performBlock[AndWait] 调用时。

    除非您别无选择,否则您不应该使用performBlockAndWait。嵌套上下文尤其成问题。

    请改用performBlock

    【讨论】:

    • 不应该在看足球的时候发帖(阿肯色州/堪萨斯州)...分心了,到我半场发帖的时候,马库斯已经回答了...但是直到我才看到在我发布之后。对不起,多余的帖子。
    • 谢谢。说得通。我不应该使用 performBlockAndWait。但是如果我必须使用,我至少不应该从主线程调用它。
    猜你喜欢
    • 2015-08-28
    • 2012-09-12
    • 2015-12-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-31
    • 1970-01-01
    相关资源
    最近更新 更多