2013-04-16 6 views
4

Я столкнулся с такой же проблемой взаимоблокировки (что довольно часто встречается в SO), которая встречается в многоадресном сценарии NSManagedObjectContexts &. В некоторых контроллерах моего приложения мое приложение использует фоновые потоки для получения данных из веб-службы, и в этом же потоке он сохраняет его. В других случаях, когда имеет смысл не продвигаться дальше без сохранения (например, сохраняйте значения из формы, когда они нажимают «Далее»), сохранение выполняется в основном потоке. AFAIK не должно быть ничего плохого в теории, но иногда я могу сделать тупиковой случиться на призывСинтаксис CoreData с несколькими потоками

if (![moc save:&error]) 

... и это, кажется, всегда на фоне потока сохранить, когда происходит затор. Это не происходит при каждом звонке; на самом деле все наоборот: я должен использовать свое приложение на пару минут, а потом это произойдет.

Я прочитал все сообщения, которые я мог найти, а также документы Apple и т. Д., И я уверен, что следую рекомендациям. Конкретно, мое понимание работы с несколькими MOC/потоками сводится к:

  • Каждая нить должна иметь свой собственный MOC.
  • В этом потоке должен быть создан MOC потока, который не передается из одного потока в другой.
  • NSManagedObject не может быть передан, но NSManagedObjectID может, и вы используете ID для раздувания NSManagedObject с использованием другого MOC.
  • Изменения от одного MOC должны быть объединены с другим, если они оба используют тот же PersistentStoreCoordinator.

Некоторое время назад я наткнулся на некоторый код для MOC вспомогательного класса на this SO thread и обнаружил, что это было легко понять, и очень удобно в использовании, поэтому все мое MOC взаимодействие теперь через это. Вот мой ManagedObjectContextHelper класс во всей своей полноте:

#import "ManagedObjectContextHelper.h" 

@implementation ManagedObjectContextHelper 

+(void)initialize { 
    [[NSNotificationCenter defaultCenter] addObserver:[self class] 
              selector:@selector(threadExit:) 
               name:NSThreadWillExitNotification 
               object:nil]; 
} 

+(void)threadExit:(NSNotification *)aNotification { 
    TDAppDelegate *delegate = (TDAppDelegate *)[[UIApplication sharedApplication] delegate]; 
    NSString *threadKey = [NSString stringWithFormat:@"%p", [NSThread currentThread]]; 
    NSMutableDictionary *managedObjectContexts = delegate.managedObjectContexts; 

    [managedObjectContexts removeObjectForKey:threadKey]; 
} 

+(NSManagedObjectContext *)managedObjectContext { 
    TDAppDelegate *delegate = (TDAppDelegate *)[[UIApplication sharedApplication] delegate]; 
    NSManagedObjectContext *moc = delegate.managedObjectContext; 

    NSThread *thread = [NSThread currentThread]; 

    if ([thread isMainThread]) { 
     [moc setMergePolicy:NSErrorMergePolicy]; 
     return moc; 
    } 

    // a key to cache the context for the given thread 
    NSString *threadKey = [NSString stringWithFormat:@"%p", thread]; 

    // delegate.managedObjectContexts is a mutable dictionary in the app delegate 
    NSMutableDictionary *managedObjectContexts = delegate.managedObjectContexts; 

    if ([managedObjectContexts objectForKey:threadKey] == nil) { 
     // create a context for this thread 
     NSManagedObjectContext *threadContext = [[NSManagedObjectContext alloc] init]; 
     [threadContext setPersistentStoreCoordinator:[moc persistentStoreCoordinator]]; 
     [threadContext setMergePolicy:NSErrorMergePolicy]; 
     // cache the context for this thread 
     NSLog(@"Adding a new thread:%@", threadKey); 
     [managedObjectContexts setObject:threadContext forKey:threadKey]; 
    } 

    return [managedObjectContexts objectForKey:threadKey]; 
} 

+(void)commit { 
    // get the moc for this thread 
    NSManagedObjectContext *moc = [self managedObjectContext]; 

    NSThread *thread = [NSThread currentThread]; 

    if ([thread isMainThread] == NO) { 
     // only observe notifications other than the main thread 
     [[NSNotificationCenter defaultCenter] addObserver:[self class]             selector:@selector(contextDidSave:) 
                name:NSManagedObjectContextDidSaveNotification 
                object:moc]; 
    } 

    NSError *error; 
    if (![moc save:&error]) { 
     NSLog(@"Failure is happening on %@ thread",[thread isMainThread][email protected]"main":@"other"); 

     NSArray* detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey]; 
     if(detailedErrors != nil && [detailedErrors count] > 0) { 
      for(NSError* detailedError in detailedErrors) { 
       NSLog(@" DetailedError: %@", [detailedError userInfo]); 
      } 
     } 
     NSLog(@" %@", [error userInfo]); 

    } 

    if ([thread isMainThread] == NO) { 
     [[NSNotificationCenter defaultCenter] removeObserver:[self class]              name:NSManagedObjectContextDidSaveNotification 
                 object:moc]; 
    } 
} 

+(void)contextDidSave:(NSNotification*)saveNotification { 
    TDAppDelegate *delegate = (TDAppDelegate *)[[UIApplication sharedApplication] delegate]; 
    NSManagedObjectContext *moc = delegate.managedObjectContext; 

    [moc performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) 
          withObject:saveNotification 
         waitUntilDone:NO]; 
} 
@end 

Вот фрагмент многопоточной бит, где он, кажется тупиковой:

NSManagedObjectID *parentObjectID = [parent objectID]; 

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); 
    dispatch_async(queue, ^{ 
     // GET BACKGROUND MOC 
     NSManagedObjectContext *backgroundContext = [ManagedObjectContextHelper managedObjectContext]; 

     Parent *backgroundParent = (Parent*)[backgroundContext objectWithID:parentObjectID]; 
     // HIT THE WEBSERVICE AND PUT THE RESULTS IN THE PARENT OBJECT AND ITS CHILDREN, THEN SAVE... 
[ManagedObjectContextHelper commit]; 

     dispatch_sync(dispatch_get_main_queue(), ^{ 

      NSManagedObjectContext *mainManagedObjectContext = [ManagedObjectContextHelper managedObjectContext]; 

      parent = (Parent*)[mainManagedObjectContext objectWithID:parentObjectID]; 
}); 
    }); 

conflictList в ошибке, кажется, предполагает, что это что-то делать с ObjectID родительского объекта:

conflictList =  (
      "NSMergeConflict (0x856b130) for NSManagedObject (0x93a60e0) with objectID '0xb07a6c0 <x-coredata://B7371EA1-2532-4D2B-8F3A-E09B56CC04F3/Child/p4>' 
with oldVersion = 21 and newVersion = 22 
and old object snapshot = {\n parent = \"0xb192280 <x-coredata://B7371EA1-2532-4D2B-8F3A-E09B56CC04F3/Parent/p3>\";\n name = \"New Child\";\n returnedChildId = 337046373;\n time = 38;\n} 
and new cached row = {\n parent = \"0x856b000 <x-coredata://B7371EA1-2532-4D2B-8F3A-E09B56CC04F3/Parent/p3>\";\n name = \"New Child\";\n returnedChildId = 337046373;\n time = 38;\n}" 
     ); 

Я попытался положить в refreshObject звонки, как только я получил достать MOC, с теорией будучи го если это MOC, который мы использовали раньше (например, мы использовали MOC в основном потоке раньше, и вполне вероятно, что это то же самое, что и вспомогательный класс), тогда, возможно, сохранение в другом потоке означает, что нам нужно явно обновить. Но это не имело никакого значения, это все еще тупики, если я продолжаю нажимать достаточно долго.

У кого-нибудь есть идеи?

Редактирование: Если у меня установлена ​​точка останова для всех исключений, отладчик автоматически приостанавливается на линии if (![moc save:&error]), поэтому кнопка воспроизведения/паузы уже приостановлена ​​и отображает треугольник воспроизведения. Если я отключу точку останова для всех исключений, тогда она фактически регистрирует конфликт и продолжается - вероятно, потому, что в настоящее время политика слияния установлена ​​на NSErrorMergePolicy, поэтому я не думаю, что это фактически блокировка потоков. Here's a screehshot состояния обоих потоков во время паузы.

+0

Когда ваше приложение блокируется, в каком методе он висит на основной теме? – lassej

+0

@lassej Вот распечатка Thread1: libsystem_kernel.dylib'mach_msg_trap: 0x98030c18: movl $ 4294967265,% eax 0x98030c1d: calll 0x9803449a; _sysenter_trap 0x98030c22: ret 0x98030c23: nop – bobsmells

+0

Я не уверен, что это то, что вам нужно. Thread6 включен в [NSManagedObjectContext save]. Я не уверен, что Thread1 фактически заблокирован, но это тупик на MOC двух потоков, если я правильно понял. – bobsmells

ответ

3

Я не рекомендую ваш подход вообще.Во-первых, если вы не ограничены iOS4, вы должны использовать тип параллелизма MOC, а не старый метод. Даже под iOS 5 (который разбит на вложенные контексты) подход performBlock гораздо более звучит.

Также обратите внимание, что dispatch_get_global_queue предоставляет параллельную очередь, которая не может использоваться для синхронизации.

Подробности можно найти здесь: http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/CoreData/Articles/cdConcurrency.html

Редактировать

Вы пытаетесь управлять МОС и нарезания резьбы вручную. Вы можете сделать это, если хотите, но на вашем пути будут драконы. Вот почему был создан новый способ, чтобы свести к минимуму вероятность ошибок при использовании Core Data в нескольких потоках. В любое время, когда я вижу ручное управление потоками с помощью Core Data, я всегда предлагаю изменить его как первый подход. Это избавит вас от большинства ошибок.

Мне не нужно видеть гораздо больше, чем вы вручную сопоставляете MOC и нити, чтобы знать, что вы просите о неприятностях. Просто перечитайте эту документацию и сделайте это правильно (используя performBlock).

+0

Спасибо @Jody Hagins, хотя я не совсем понимаю, о чем вы говорите. Я прочитал эту ссылку, и AFAIK это в значительной степени соответствует тому, что я делаю (так что я явно чего-то пропустил), но это то, что я делаю «старый метод»? В чем разница с типом параллелизма MOC? Каков тип параллелизма MOC? – bobsmells

+0

ОК, извините, я понимаю о типе параллелизма MOC, но я не понимаю, почему это все еще не работает так, как есть. См., Что касается вашей точки в глобальной очереди и синхронизации, в моем приложении никогда не работают операции с базами данных, запущенные одновременно на нескольких потоках, поэтому я думаю, что это не должно быть проблемой, не так ли? Я использую только GCD, поэтому я могу обновить пользовательский интерфейс (например, индикатор выполнения) при длительном удалении веб-сервисов. – bobsmells

+0

Спасибо, пенни начинает падать ... но проблема в том, что мне нужно использовать многопоточность (через GCD в моем случае) по другой причине - главным образом, длительный вызов веб-службы, поэтому как выполнить функцию executeBlock и выполнитьBlockAndWait вписывается в это? Учитывая, что это методы в MOC, могу ли я поместить вызов веб-службы внутри executeBlock, а затем сохранить результирующие данные сразу после этого, все еще внутри блока? Как насчет обновлений моего пользовательского интерфейса, которые зависят от этих изменений данных? Это случай наличия внешнего nonMainMOC performBlock, который имеет mainMOC выполнитьBlock внутри внешнего? – bobsmells

Смежные вопросы