Общий подход к NSManagedObjectContext в многопоточном приложении

Я читал несколько сообщений здесь о NSManagedObjectContext и многопоточных приложениях. Я также рассмотрел пример CoreDataBooks, чтобы понять, как отдельные потоки требуют собственного NSManagedObjectContext и как операция сохранения объединяется с основным NSManagedObjectContext. Я нашел пример хорошим, но также и специфичным для приложения. Я пытаюсь обобщить это и задаюсь вопросом, звучит ли мой подход.

Мой подход состоит в том, чтобы иметь общую функцию для извлечения NSManagedObjectContext для текущего потока. Функция возвращает NSManagedObjectContext для основного потока, но создаст новый (или извлечет его из кеша), если вызвана из другого потока. Это выглядит следующим образом:

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

    NSThread *thread = [NSThread currentThread];

    if ([thread isMainThread]) {
        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] autorelease];
        [threadContext setPersistentStoreCoordinator:[moc persistentStoreCoordinator]];
        // cache the context for this thread
        [managedObjectContexts setObject:threadContext forKey:threadKey];
    }

    return [managedObjectContexts objectForKey:threadKey];
}

Операции сохранения выполняются просто, если вызывается из основного потока. Операции сохранения, вызываемые из других потоков, требуют объединения в основном потоке. Для этого у меня есть общая функция commit:

+(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
                                             selector:@selector(contextDidSave:)
                                                 name:NSManagedObjectContextDidSaveNotification
                                               object:moc];
    }

    NSError *error;
    if (![moc save:&error]) {
        // fail
    }

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

В функции contextDidSave: мы выполняем слияние, если вызвано уведомлением в commit.

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

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

Наконец, мы очищаем кеш NSManagedObjectContext с помощью этого:

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

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

    [managedObjectContexts removeObjectForKey:threadKey];
}

Это компилируется и, кажется, работает, но я знаю, что проблемы с потоками могут быть сложными из-за условий гонки. Кто-нибудь видит проблему с этим подходом?

Кроме того, я использую это в контексте асинхронного запроса (используя ASIHTTPRequest), который извлекает некоторые данные с сервера и обновляет и вставляет хранилище на iPhone. Кажется, что NSThreadWillExitNotification не запускается после завершения запроса, и тот же поток затем используется для последующих запросов. Это означает, что один и тот же NSManagedObjectContext используется для отдельных запросов в одном потоке. Это проблема?

Ответы

Ответ 1

Через год после публикации этого вопроса я, наконец, создал структуру для обобщения и упрощения работы с Core Data. Это выходит за рамки первоначального вопроса и добавляет ряд функций, позволяющих значительно упростить взаимодействие с Core Data. Подробности здесь: https://github.com/chriscdn/RHManagedObject

Ответ 2

Я нашел решение, наконец, поняв проблему лучше. Мое решение напрямую не затрагивает вопрос выше, но решает проблему того, почему мне приходилось иметь дело с потоками в первую очередь.

Мое приложение использует библиотеку ASIHTTPRequest для асинхронных запросов. Я извлекаю некоторые данные с сервера и использую функцию делегата requestFinished для добавления/изменения/удаления объектов основных данных. Функция requestFinished выполнялась в другом потоке, и я предположил, что это был естественный побочный эффект асинхронных запросов.

После углубления я обнаружил, что ASIHTTPRequest намеренно запускает запрос в отдельном потоке, но может быть переопределен в моем подклассе ASIHTTPRequest:

+(NSThread *)threadForRequest:(ASIHTTPRequest *)request {
    return [NSThread mainThread];
}

Это небольшое изменение помещает requestFinished в основной поток, что избавило меня от необходимости заботиться о потоках в моем приложении.