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】:

您的实施存在许多问题。

    方法diskManagedObjectContextbackgroundManagedObjectContext

    使用全局并发队列,这是没有意义的。如果你想让方法线程安全(考虑到实例变量是共享资源),你需要使用 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) 死锁的主要内容,如果未能解决你的问题,请参考以下文章

多个 ManagedObjectContext

核心数据,@Environment(\.managedObjectContext),onMove

AppDelegate 类型的值没有成员 managedObjectContext

将 managedObjectContext 发送到 viewController 崩溃

处理 managedObjectContext 的核心数据错误

NSManagedObject prepareForDeletion 中的 self.managedObjectContext == nil 有啥意义?