核心数据嵌套的托管对象上下文和频繁死锁/卡顿

32

我有一个几乎与这个人描述的问题完全相同的问题,但它还没有得到答案:

http://www.cocoabuilder.com/archive/cocoa/312683-core-data-nested-managed-object-contexts-and-frequent-deadlocks.html#312683

问题如下:

我已经设置了一个拥有NSPrivateQueueConcurrencyType和持久存储协调器的父MOC,并且设置了一个使用NSMainQueueConcurrencyType的子MOC。其想法是,大部分长时间而艰难的工作和保存可以在私有MOC上完成,从而释放主线程以防止阻塞UI。不幸的是,似乎遇到了一些会导致死锁的情况。

如果子MOC(在主线程上)正在使用NSFetchedResultsController执行取回操作,则将向父上下文发送-executeFetchRequest:可能会造成死锁。虽然文档似乎表明在主线程上使用具有主线程并发类型的MOC而不使用performBlock:也是可以的。

似乎私有队列正在等待PSC的锁,而主线程上的子上下文已经锁定了该锁。似乎子上下文(在持有PSC锁的同时)正在尝试dispatch_sync到父上下文,因此它们都在等待对方。

PriveQueue-> MainQueue是受支持的配置吗?似乎大多数人仍然将父上下文放在主线程上。

主线程看起来像这样:

> #0    0x960f6c5e in semaphore_wait_trap ()
> #1    0x04956bb5 in _dispatch_thread_semaphore_wait ()
> #2    0x04955c8f in _dispatch_barrier_sync_f_slow ()
> #3    0x04955dea in dispatch_barrier_sync_f ()
> #4    0x01797de5 in _perform ()
> #5    0x01798547 in -[NSManagedObjectContext(_NestedContextSupport) newValuesForObjectWithID:withContext:error:] ()
> #6    0x0176416b in _PFFaultHandlerLookupRow ()
> #7    0x01763f97 in -[NSFaultHandler fulfillFault:withContext:forIndex:] ()
> #8    0x01763b75 in _PF_FulfillDeferredFault ()
> #9    0x017639f2 in _sharedIMPL_pvfk_core ()
> #10    0x017681a0 in _pvfk_11 ()
> #11    0x0001b322 in -[FBUser sectionName] at /Users/mlink/Code/x/x/FBUser.m:62
> #12    0x011a8813 in _NSGetUsingKeyValueGetter ()
> #13    0x017a0652 in -[NSManagedObject valueForKey:] ()
> #14    0x011ab8d5 in -[NSObject(NSKeyValueCoding) valueForKeyPath:] ()
> #15    0x01851f72 in -[NSFetchedResultsController(PrivateMethods) _sectionNameForObject:] ()
> #16    0x01853af6 in -[NSFetchedResultsController(PrivateMethods) _computeSectionInfo:error:] ()
> #17    0x01850ea6 in -[NSFetchedResultsController performFetch:] ()
> #18    0x0003a4fc in __62-[SYFriendsTableViewController updateFetchedResultsController]_block_invoke_0 ()
> #19    0x01797af3 in developerSubmittedBlockToNSManagedObjectContextPerform ()
> #20    0x049554f0 in _dispatch_main_queue_callback_4CF ()
> #21    0x01b3e833 in __CFRunLoopRun ()
> #22    0x01b3ddb4 in CFRunLoopRunSpecific ()
> #23    0x01b3dccb in CFRunLoopRunInMode ()
> #24    0x023d6879 in GSEventRunModal ()
> #25    0x023d693e in GSEventRun ()
> #26    0x0089aa9b in UIApplicationMain ()
> #27    0x00002656 in main at /Users/mlink/Code/x/x/main.mm:16

私有队列堆栈长这样:

#0    0x960f8876 in __psynch_mutexwait ()
#1    0x97e9e6af in pthread_mutex_lock ()
#2    0x0172ec22 in -[_PFLock lock] ()
#3    0x0172ebfa in -[NSPersistentStoreCoordinator lock] ()
#4    0x01746a8c in -[NSManagedObjectContext(_NSInternalAdditions) lockObjectStore] ()
#5    0x01745030 in -[NSManagedObjectContext executeFetchRequest:error:] ()
#6    0x0009d49f in -[NSManagedObjectContext(Additions) executeFetchRequest:] at /Users/mlink/Code/objc/C/C/NSManagedObjectContext+Additions.m:44
#7    0x0002177f in +[FBUser usersForFbids:inManagedObjectContext:] at /Users/mlink/Code/x/x/FBUser.m:435
#8    0x00021fc0 in __77+[FBUser updateUserFromGraphValues:inManagedObjectContext:completionHandler:]_block_invoke_0 at /Users/mlink/Code/x/x/FBUser.m:461
#9    0x0180f9f3 in developerSubmittedBlockToNSManagedObjectContextPerform_privateasync ()
#10    0x04954ecf in _dispatch_queue_drain ()
#11    0x04954d28 in _dispatch_queue_invoke ()
#12    0x049544af in _dispatch_worker_thread2 ()
#13    0x97ea1b24 in _pthread_wqthread ()
#14    0x97ea36fe in start_wqthread ()

他还写了这句话:

我开始认为问题出在 NSFetchedResultsController 上,当死锁发生时它总是被卡在 performFetch: 上。大部分时间它都会卡在尝试获取一个对象的 section 名称时,试图将其加载入内存。作为测试,我尝试重现 FRC 所做的操作,执行了 executeFetchRequest: 方法并遍历结果,逐个询问每个对象的 section 名称,但这并没有导致死锁。如果我让 FRC 在我的测试之后继续执行 performFetch:,它仍然会卡在那里。我有 99% 的把握认为 FRC 存在与嵌套上下文同步的问题。

问题:有人知道这个问题为什么会发生吗?您知道如何解决它吗?这是一个 bug 吗?


你是否向父上下文发送“-executeFetchRequest”?为什么? - Martin R
由于后台线程(背景/父MOC)正在将数据同步到服务器,因此我正在执行获取请求以查找需要同步的对象。 - thejaz
2
+1同样的问题。对于嵌套上下文,我正在使用MagicalRecord的主和根保存上下文。在调用FRC performFetch之前,我启动了一个与服务器的同步请求。代码在performFetch处冻结。 - angelokh
@thejaz,可以问一下你是如何获取堆栈跟踪文本的吗?我知道如何生成堆栈并在Xcode中查看它。但是我无法将其复制粘贴到其他地方。 - Philip007
7个回答

35
我刚刚阅读了这篇Stack Overflow文章,在其中,法布里斯特鲁洛德尚布里耶建议目前不要使用嵌套上下文。他提供了一篇文章Core Data Growing Pains作为参考。
从那篇文章中可以看到:
NSFetchedResultsController死锁 你绝不希望你的应用程序死锁。使用NSFetchedResultsController和嵌套上下文,它很容易发生。使用与以上描述相同的UIManagedDocument设置,在私有队列上下文中执行获取请求,同时在主队列上下文中使用NSFetchedResultsController将很可能导致死锁。如果你同时启动两者,它几乎百分之百会发生。NSFetchedResultsController可能正在获取不应该获取的锁。这已经报告为即将发布的iOS版本修复。
radar://11861499 在即将发布的版本中修复

欢迎。那篇文章真的很有信息量。我们应该给Fabrice点赞,以示感谢他提供了参考资料。 - Martin R
Joerg:我没有进行彻底的测试,但我的锁定问题在iOS 5.1中仍然存在,但在iOS 6.0中已经解决。 - chris
2
这个问题已经被报告并在即将发布的iOS版本中修复。- 真的吗,现在是iOS 9,但这个bug仍然存在于iOS中。 - Yuri
我仍然看到这个确切的问题。有没有已知的解决方法? - AnthonyMDev
多么惊人,我们已经升级到iOS 15.3了,但这个问题仍然存在。 - Kemal Can Kaynak
显示剩余5条评论

2
对于我的iOS 6应用程序,我与OP有相同的并发设置 - 父MOC使用私有队列,子MOC在主线程上。我还有一个NSFetchedResultsController,它使用子MOC来更新UITableViewController。这些MOC都在AppDelegate中初始化,并且要在整个应用程序中使用。AppDelegate有两种方法,savePrivateThreadMOCToCoreDatasaveMainThreadMOCToCoreData,用于将更改持久化到CD。在启动时,我在私有队列上调度coredata初始化器,如下所示。其想法是立即将用户放入表视图,并允许初始化器在后台更新核心数据。
    dispatch_async(private_queue,^{
        [CoreDataInitializer initialize];
    });

最初,当savePrivateThreadMOCToCoreData在-performBlock中执行保存时,我看到了与上面链接的“Core Data Growing Pains”描述相同的psynch_mutex死锁。如果我尝试在保存正在进行时将数据读入TableVC,我还会看到崩溃。
    Collection <__NSCFSet: 0x7d8ea90> was mutated while being enumerated.

为了解决这些问题,我改用 -performBlockAndWait 来进行保存操作。我不再遇到死锁和崩溃,但是让 UI 等待保存操作并不合适。最后,我删除了所有对 -performBlock* 的调用,使用了一个普通的 [privateMOC save:&error],然后所有问题都消失了。获取的结果控制器可以清晰地读取部分已保存的数据并更新表格,不再有死锁或“在枚举时发生变异”的错误。
我怀疑 -performBlock* 应该由其他线程使用,这些线程没有创建相关的 MOC,以请求在其上执行操作。由于我的私有线程和主线程 MOC 均属于应用程序委托,因此在私有 MOC 上进行保存操作不应该使用 -performBlock*。
可能值得注意的是,尽管我的构建环境是 iOS 6,但基础部署目标 SDK 是 iOS 5.0。似乎其他人在 iOS 6 上不再遇到这个问题了。

我怀疑了同样的事情,但没有证据。 - user4951

0

我也遇到了与 developerSubmittedBlockToNSManagedObjectContextPerform 相关的崩溃。

在我的情况下,请考虑以下方法调用模式:

[privatecontext performBlock:^{
    A(CDManager.privatecontext);
}];

其中:

A(CDManager.privateContext)调用B() B()调用C() C()调用D()

并且:

方法A()和方法C()包含一些Core Data操作。 A()已经知道要在哪个上下文中工作,但A()不会通知B()关于上下文的信息,因此C()也没有任何关于要在哪个上下文中工作的信息,所以C()在默认上下文(main)中工作。这会导致由于数据库中数据不一致而崩溃。

修复:

所有要进行数据库操作的方法都应该使用它们要处理的上下文进行参数化,除了D()之外,因为它不需要进行数据库操作,例如:

A(context)调用B(context) B(context)调用C(context) C(context)调用D()


0

我解决了由两个线程同时获取数据引起的死锁问题(BG执行fetchedRequest,MAIN执行NSFRC的fetch)。解决方案是为长时间运行的同步操作创建一个新的上下文。它没有父上下文,具有并发类型NSPrivateQueueConcurrencyType,并直接链接到公共PSC。在此背景下完成所有长时间运行的工作后,我将其保存并通过使用mergeChangesFromContextDidSaveNotification例程将其与其余并行上下文堆栈合并。

Magical Record 3中实现了一个很好的解决方案。在这里查看更多信息:https://dev59.com/aILba4cB1Zd3GeqPYwjf#25060126


0

这个问题发生在我的代码中,因为父上下文使用了NSMainQueueConcurrencyType。

为了解决这个问题,我将主队列上的ManagedObjectContext设置为一个子级。在每次加载东西之前,我都会调用reset方法以确保主队列上的数据与父级保持一致。但通常情况下,它们并不相同。


-1
“这个想法是大部分的长期艰苦工作和存储都可以在私有MOC上完成。”你如何实现这个想法?你会使用类似这样的东西吗:
- (void)doSomethingWithDocument:(UIManagedDocument *)document
{
    NSManagedObjectContext *parent = document.managedObjectContext.parentContext;
        [parent performBlock:^{
            /* 
               Long and expensive tasks.. 
               execute fetch request on parent context
               download from remote server               
            */
            // save document
        }];
}

我之前也尝试过这种方法,但最终也遇到了死锁的问题。后来我尝试不去操作父级上下文的后备队列,而是使用简单明了的GCD来进行下载操作,并在子级上下文(主队列)中操作核心数据。这样做可以正常工作,虽然看起来父级上下文似乎没什么用处,但至少不会导致死锁。

- (void)doSomethingWithDocument:(UIManagedDocument *)document
{
    dispatch_queue_t fetchQ = dispatch_queue_create("Flickr fetcher", NULL);
    dispatch_async(fetchQ, ^{
        // download from remote server        
        // perform in the NSMOC's safe thread (main thread)
        [document.managedObjectContext performBlock:^{ 
            // execute fetch request on parent context
            // save document  
        }];
    });
    dispatch_release(fetchQ);
}

document.managedObjectContext.parentContext执行块是一个不好的主意。所有与Tableview相关的东西都会失败。例如,你会收到以下错误:[UITableView _endCellAnimationsWithContext:]中的断言失败。 - coolcool1994
这实际上是一个TableView数据源的问题,而不是Core Data的问题。你所做的状态转换对TableView状态引擎来说没有意义。这通常是因为数据源没有正确实现(例如,在空状态下返回0个section,然后某些东西插入了一个section...崩溃)。 - quellish

-1

我想要表达同意避免嵌套上下文的观点。我一直在使用iOS 7中的嵌套上下文(主队列子上下文和私有队列父上下文)以及NSFetchedResultsControllers,但是出现了无法解决的死锁问题。我改用独立的MOCs和保存通知,问题就消失了。

如果有人需要快速指南来更改他们的代码,这个页面有准备好的代码(只需忽略嵌套上下文的建议):

http://www.cocoanetics.com/2012/07/multi-context-coredata/


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