2013-03-13 42 views
2

一個典型的設置:我們有一個mainMOC的主線程和一個自己的後臺線程backgroundMOC。後臺線程通過將塊調度到backgroundQueue來對backgroundMOC執行只讀操作。如何在跨線程合併更改時防止競爭條件?

backgroundMOC需要合併從mainMOC的變化,所以我們註冊NSManagedObjectContextDidSaveNotification,然後像做

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

比方說,用戶刪除的mainMOC的對象。上面的代碼對我來說似乎並不安全,因爲合併將在未來的某個時間點完成。在合併完成之前,backgroundQueue上的塊可能仍會嘗試使用已刪除的對象。

明顯的解決方案是使用dispatch_sync代替(或performBlockAndWait,performSelector:OnThread:...)。從我在互聯網上看到的代碼片段來看,這似乎是每個人都在做的事情。但是我對這個解決方案也不舒服。

名稱NSManagedObjectContextDidSaveNotification意味着保存已在通知發送時發生。所以相應的行已經從底層數據庫中刪除(假設一個sqlite存儲)。 dispatch_sync必須等待隊列上的其他塊完成才能合併更改,而這些其他塊仍然可以嘗試使用已刪除的對象,從而產生NSObjectInaccessibleException

在我看來,要合併從一個線程/隊列到另一個變化的正確方法是

  1. 訂閱NSManagedObjectContextWillSaveNotificationNSManagedObjectContextDidSaveNotification在後臺線程。
  2. NSManagedObjectContextWillSaveNotification:清空backgroundQueue並暫停向隊列分派新塊的任何操作。
  3. NSManagedObjectContextDidSaveNotification:同步合併更改。
  4. 恢復對後臺隊列的正常操作。

這是正確的方法還是我錯過了什麼?

+0

作爲後續:我不斷收到死鎖與上面的解決方案。看起來在某些情況下,主控線程在發送'NSManagedObjectContextWillSaveNotification'時已經獲得了PSC上的鎖定,這可能導致上面步驟2中所需的dispatch_sync調用在背景隊列上仍有未完成的任務時一直等待。 – Lukas 2013-03-26 12:48:28

回答

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