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

时间:2013-03-13 09:53:38

标签: multithreading core-data merge race-condition

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

backgroundMOC需要合并mainMOC的更改,因此我们注册NSManagedObjectContextDidSaveNotification,然后执行类似

的操作
- (void)mainMocDidSave:(NSNotification *)notification {
    dispatch_async(backgroundQueue, ^{
        [backgroundMoc mergeChangesFromContextDidSaveNotification:notification];
    });
}

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

显而易见的解决方案是改为使用dispatch_sync(或performBlockAndWaitperformSelector:OnThread:...)。从我在互联网上看到的代码片段来看,这似乎是每个人都在做的事情。但我对这个解决方案也不满意。

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

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

  1. 在后台主题上订阅NSManagedObjectContextWillSaveNotificationNSManagedObjectContextDidSaveNotification
  2. 开启NSManagedObjectContextWillSaveNotification:清空backgroundQueue并暂停任何将新块分配到队列的操作。
  3. 开启NSManagedObjectContextDidSaveNotification:同步合并更改。
  4. 恢复后台队列的正常操作。
  5. 这是正确的做法还是我错过了什么?

1 个答案:

答案 0 :(得分:0)

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

<强> 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];
}