跨线程合并更改时如何防止竞争条件?

Posted

技术标签:

【中文标题】跨线程合并更改时如何防止竞争条件?【英文标题】:How to prevent race conditions when merging changes across threads? 【发布时间】:2013-03-13 09:53:38 【问题描述】:

一个典型的设置:我们有一个带有mainMOC 的主线程和一个带有自己的backgroundMOC 的后台线程。后台线程通过将块分派到backgroundQueuebackgroundMOC 执行只读操作。

backgroundMOC 需要合并来自mainMOC 的更改所以我们注册NSManagedObjectContextDidSaveNotification 然后做类似的事情

- (void)mainMocDidSave:(NSNotification *)notification 
    dispatch_async(backgroundQueue, ^
        [backgroundMoc mergeChangesFromContextDidSaveNotification:notification];
    );

假设用户删除了mainMOC 中的一个对象。上面的代码对我来说似乎并不安全,因为合并将在未来的某个时候完成。在合并完成之前,backgroundQueue 上可能仍有块试图使用已删除的对象。

显而易见的解决方案是改用dispatch_sync(或performBlockAndWaitperformSelector:OnThread:...)。从我在互联网上看到的代码 sn-ps 来看,这似乎是每个人都在做的事情。但我也不喜欢这种解决方案。

名称NSManagedObjectContextDidSaveNotification 表示在发送通知时已经进行了保存。所以相应的行已经从底层数据库中删除(假设是一个 sqlite 存储)。 dispatch_sync 必须等待队列中的其他块完成才能合并更改,而这些其他块仍可能尝试使用已删除的对象,从而导致NSObjectInaccessibleException

在我看来,将更改从一个线程/队列合并到另一个的正确方法是

    在后台线程上订阅NSManagedObjectContextWillSaveNotificationNSManagedObjectContextDidSaveNotification。 在NSManagedObjectContextWillSaveNotification 上:清空backgroundQueue 并暂停任何将新块分派到队列的操作。 开启NSManagedObjectContextDidSaveNotification:同步合并更改。 在后台队列上恢复正常操作。

这是正确的方法还是我遗漏了什么?

【问题讨论】:

作为跟进:我不断遇到上述解决方案的死锁。似乎在某些情况下,主线程在发送NSManagedObjectContextWillSaveNotification 时已经获得了 PSC 上的锁,这可能导致上面第 2 步中所需的 dispatch_sync 调用在后台队列上仍有未完成的任务时永远等待。 【参考方案1】:

我在两个遇到与您类似的问题的项目中使用了以下结构。首先,我使用单例服务来确保只有一个后台线程合并和读取更改。

AppDelegate.m

- (NSManagedObjectContext *)managedObjectContext 
    if (_managedObjectContext != nil) 
        return _managedObjectContext;
    

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) 
        // It is crucial to use the correct concurrency type!
        _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    
    return _managedObjectContext;


- (void)saveContext 
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil) 
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) 
             // Replace this implementation with code to handle the error appropriately.
             // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        
        else 
            [[NSNotificationCenter defaultCenter] postNotificationName:@"ParentContextDidSaveNotification" object:nil];
        
    

BackgroundService.m

- (id)init 
    self = [super init];

    if (self) 
        [self managedObjectContext];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(parentContextDidSave) name:@"ParentContextDidSaveNotification" object:nil];
    

    return self;


- (NSManagedObjectContext *)managedObjectContext 
    if (!_managedObjectContext) 
        // Again, make sure you use the correct concurrency type!
        _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [_managedObjectContext setParentContext:[(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext]];
    

    return _managedObjectContext;



- (BOOL)saveContext 
    @synchronized(self) 
        BOOL successful = YES;

        // Bad practice, process errors appropriately.
        [[self managedObjectContext] save:nil];

        [[[self managedObjectContext] parentContext] performBlock:^
            [(AppDelegate *)[[UIApplication sharedApplication] delegate] saveContext];
        ];

        return successful;
    


- (void)parentContextDidSave 
    [[self managedObjectContext] reset];

    [[NSNotificationCenter defaultCenter] postNotificationName:@"ManagedObjectContextResetNotification" object:nil];

【讨论】:

以上是关于跨线程合并更改时如何防止竞争条件?的主要内容,如果未能解决你的问题,请参考以下文章

跨线程复制 boost::exception 崩溃

C# winform 跨线程更改窗体控件的属性

Java MySQL 防止竞争条件

跨线程共享内存访问

c#中如何跨线程调用windows控件

使用委托进行跨线程处理的 C# 问题