ManagedObjectContext performBlock(AndWait) 死锁
Posted
技术标签:
【中文标题】ManagedObjectContext performBlock(AndWait) 死锁【英文标题】:ManagedObjectContext performBlock(AndWait) deadlock 【发布时间】:2014-04-08 20:02:13 【问题描述】:我之前已经看到过这个问题,但似乎没有一个解决方案对我的情况有任何影响,那就是:
我的应用使用三个 ManagedObjectContext:
1) 在全局(后台)队列上创建具有 NSPrivateQueueConcurrencyType 且没有父上下文的“diskManagedObjectContext”,并用于在后台线程上将上下文更改写入磁盘(持久存储):
-(NSManagedObjectContext *)diskManagedObjectContext
if (! _diskManagedObjectContext)
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^
_diskManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_diskManagedObjectContext.persistentStoreCoordinator = self.ircManagedObjectLibrary.persistentStoreCoordinator;
);
return _diskManagedObjectContext;
2) 带有 NSMainQueueConcurrencyType 的“mainManagedObjectContext”将 diskManagedObjectContext 作为其父上下文,在主线程上创建(在应用程序启动时)并由所有 GUI 进程(视图控制器等)使用。保存在 mainManagedObjectContext 上,只需将更改“向上”推送到其父对象 diskManagedObjectContext,然后它会将更改异步写入磁盘。
-(NSManagedObjectContext *)mainManagedObjectContext
if (! _mainManagedObjectContext)
_mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_mainManagedObjectContext.undoManager = nil;
_mainManagedObjectContext.parentContext = self.diskManagedObjectContext;
return _mainManagedObjectContext;
3) 带有 NSPrivateQueueConcurrencyType 的“backgroundManagedObjectContext”以 mainManagedObjectContext 作为其父上下文,在全局(后台)队列上创建,并被所有非 GUI 进程(数据归档器、日志记录等)用于背景中相对低优先级的模型变化。保存 backgroundManagedObjectContext 只需将更改“向上”推送到其父级 mainManagedObjectContext,此时监听与它们相关的模型更改的 GUI 元素会获取事件并相应地更新。
-(NSManagedObjectContext *)backgroundManagedObjectContext
if (! _backgroundManagedObjectContext)
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^
_backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_backgroundManagedObjectContext.undoManager = nil;
_backgroundManagedObjectContext.parentContext = self.mainManagedObjectContext;
);
return _backgroundManagedObjectContext;
因此,我已经实现了上述嵌套保存行为(backgroundMOC —(sync)—> mainMOC —(async)—> diskMOC —(async)—> disk),作为我所谓的“ManagedObjectLibrarian”的一种方法类(其中有两个实例,一个持有和封装mainMOC,另一个是backgroundMOC):
-(BOOL)saveChanges:(NSError **)error
__block BOOL successful = NO;
*[DEADLOCKS HERE]* [self.managedObjectContext performBlockAndWait: *[SYNCHRONOUS]*
^
NSError *internalError = nil;
// First save any changes on the managed object context of this ManagedObjectLibrarian. If the context has a parent, this does not write changes to disk but instead simply pushes the changes to the parent context in memory, and so is very fast.
successful = [_managedObjectContext save:&internalError];
if (successful)
// If successful, then if the context of this ManagedObjectLibrarian has a parent with private queue concurrency (which only the app delegate's mainIrcManagedObjectLibrarian does), save the changes on that parent context, which if it does not itself have a parent will write the changes to disk. Because this write is performed as a block operation on a private queue, it is executed in a non-main thread.
if (_managedObjectContext.parentContext)
[_managedObjectContext.parentContext performBlock: *[ASYNCHRONOUS]*
^
if (_managedObjectContext.parentContext.concurrencyType == NSPrivateQueueConcurrencyType)
NSError *parentError = nil;
BOOL parentSuccessful = [_managedObjectContext.parentContext save:&parentError];
[Error handling, etc.]
这些嵌套保存中的第一个是在 performBlockAndWait 中执行的,原因如下:
1) 它遵循 MOC 自己的保存方法的模式,该方法返回一个 BOOL 成功结果(因此必须在初始保存完成或失败后才能返回)。 2) 由于 mainMOC 和 backgroundMOC 都有父上下文,因此它们的保存只会将更改推送到它们的父级,因此非常快(因此不需要异步执行,与 diskMOC 保存不同)。
(以防万一:我执行后台 ManagedObjectLibrarian 和主 ManagedObjectLibrarian 保存在 performBlock(AndWait) 中的原因:首先在它们各自的 MOC 上,以便我可以根据需要从任何线程执行这些保存。)
我认为,所有这些都非常典型,因此希望到目前为止一切都很好。
[序言结束]
问题是:我在
中或开始时遇到明显的死锁 [self.managedObjectContext performBlockAndWait: *[SYNCHRONOUS]*
在此 saveChanges 方法的顶部。即使对这个方法的调用 not 本身是在 performBlockAndWait: 块中执行的,也会发生这种情况——事实上,我遇到的第一个死锁是在应用启动时,在主线程上调用以下代码时:
[self.backgroundManagedObjectLibrarian saveChanges:nil];
查看发生此死锁时的线程状态,另一个阻塞线程在调用 MOC 的 save: 方法时死锁:performBlock 中的方法:
[self.dataSourceProperties.managedObjectContext performBlock: *[ASYNCHRONOUS]*
^
self.dataSourceProperties.dataArchiving.isEnabled = [NSNumber numberWithBool:_dataArchivingIsEnabled];
*[DEADLOCKS HERE]* [self.dataSourceProperties.managedObjectContext save:nil];
];
此处保存的 dataArchiving.isEnabled 模型实体属性更改是在 peformBlockAndWait 中进行的:在同一 backgroundManagedObjectLibrarian; 但是上面调用 saveChanges: 在 backgroundManagedObjectLibrarian 上,死锁是在外部并且只是在之后那个块返回。!因此,我看不到早期的异步 (peformBlock:) 保存在同步 (peformBlockAndWait:) 块中,可能会阻止此后续同步 (peformBlockAndWait:) 保存在同步块之外。
更重要的是,如果我将此模型实体属性更改保存操作从 performBlock: 更改为 performBlockAndWait:,则不会发生死锁(嗯,直到执行到达下一个异步模型实体属性更改更改保存)。
我想我当然可以完成所有异步模型实体属性更改保存同步,但我觉得我不应该这样做,特别是因为我不需要这些更改立即传播,也不为任何回报或结果而坚持。
[问题]
所以,问题:
1) 为什么我会在这里陷入僵局? 2) 我在做什么错和/或误解? 3) 管理嵌套 MOC 保存和单独的模型实体属性更改保存的正确模式是什么?
谢谢!
卡尔
【问题讨论】:
【参考方案1】:您的实施存在许多问题。
方法diskManagedObjectContext
和backgroundManagedObjectContext
使用全局并发队列,这是没有意义的。如果你想让方法线程安全(考虑到实例变量是共享资源),你需要使用 dedicated 队列(串行或并发)并使用dispatch_barrier_sync
来返回 em> 一个值,dispatch_barrier_async
写入一个值。
你的 saveChanges
方法应该是异步的
一个简单而通用的实现可能如下所示:
- (void) saveContextChainWithContext:(NSManagedObjectContext*)context
completion:(void (^)(NSError*error))completion
[context performBlock:^
NSError* error;
if ([context save:&error])
NSManagedObjectContext* ctx = [context parentContext];
if (ctx)
dispatch_async(dispatch_get_global(0,0), ^
[self saveContextChainWithContext:ctx completion:completion];
);
else
if (completion)
completion(nil);
else
if (completion)
completion(error);
];
通常,使用方法的同步阻塞版本总是容易出现死锁 - 尽管 Core Data 努力以最大努力的方式避免这种情况。
因此,尽可能考虑异步 - 而且您不会遭受死锁。在您的场景中,很容易采用非阻塞异步样式。
【讨论】:
感谢您的回复!我非常喜欢您对保存链问题的解决方案(使其与用于错误处理的完成块异步 - 非常优雅!)非常!我自己应该想到的!我不明白的一件事是您的评论,即使用全局并发队列来创建两个背景上下文“没有意义”。我不是说你错了,我只是说我不明白为什么会这样。你能启发我吗?谢谢! 我或许应该注意到,我在全局队列上创建 diskManagedObjectContext 和 backgroundManagedObjectContext 的原因是我专门使用它们并且仅在后台线程上下文中使用它们(而不是它们是线程安全的) . @CarlF.Hostetter“没有意义”,意味着如果你用NSPrivateQueueConcurrencyType
创建了一个托管对象上下文,你必须在访问上下文的地方执行代码,分别在这个上下文中注册的托管对象*在该私有队列上*,并且仅在此处使用performBlock:
或performBlockAndWait:
。将分配了 ivar 的托管对象上下文的创建包装到队列中,可能有理由使封闭方法“线程安全”,这与 ivar 的访问有关。如果这不是您的目标,则无需分派到队列。
@CarlF.Hostetter 关于线程安全的说明:利用核心数据的复杂实现将不可避免地异步。此外,各种方法可以在不同线程上并发执行。鉴于此,从头开始的 线程安全 设计将有其优势 - 因为您通常可以随时从任何地方调用任何方法。
感谢您的澄清!顺便说一句,更改为带有完成块的完全异步保存确实解决了我的死锁问题。以上是关于ManagedObjectContext performBlock(AndWait) 死锁的主要内容,如果未能解决你的问题,请参考以下文章
核心数据,@Environment(\.managedObjectContext),onMove
AppDelegate 类型的值没有成员 managedObjectContext
将 managedObjectContext 发送到 viewController 崩溃
处理 managedObjectContext 的核心数据错误
NSManagedObject prepareForDeletion 中的 self.managedObjectContext == nil 有啥意义?