Problem z zapisywaniem obiektów w tle

To, co próbuję zrobić w skrócie, to użyć kolejki w tle, aby zapisać obiekty JSON wyciągnięte z usługi internetowej do bazy danych Core Data sqlite3. Zapisywanie odbywa się w serializowanej kolejce w tle, którą utworzyłem za pomocą GCD, i jest zapisywane w dodatkowej instancji NSManagedObjectContext, która jest tworzona dla tej kolejki w tle. Po zakończeniu zapisu muszę zaktualizować instancję NSManagedObjectContext, która znajduje się w głównym wątku z nowo utworzonymi/zaktualizowanymi obiektami. Na problem, który mam jednak jest instancja NSManagedObjectContext w głównym wątku nie jest w stanie znaleźć obiektów, które zostały zapisane w kontekście tła. Poniżej znajduje się lista działań, które podejmuję z przykładami kodu. Jakieś pomysły na to, co robię źle?

  • Utwórz kolejkę tła przez GCD, Uruchom całą logikę wstępnego przetwarzania, a następnie zapisz kontekst tła w tym wątku:

.

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    if (savedObjectIDs.count > 0) {
        [savedObjectIDs removeAllObjects];
    }
    if (savedObjectClass) {
        savedObjectClass = nil;
    }

    // set the thead name
    NSThread *currentThread = [NSThread currentThread];
    [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

    // if there is not already a background context, then create one
    if (!_backgroundQueueManagedObjectContext) {
        NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
        if (coordinator != nil) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    // save the JSON dictionary starting at the upper most level of the key path, and return all created/updated objects in an array
    NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (objectIds) {
        [savedObjectIDs addObjectsFromArray:objectIds];
    }
    if (completion) {
        saveCompletionBlock = completion;
    }
    if (managedObjectClass) {
        savedObjectClass = managedObjectClass;
    }

    // save all changes object context
    [self saveManagedObjectContext];
});
  • Metoda "saveManagedObjectContext" zasadniczo wygląda na który wątek jest uruchomiony i zapisuje odpowiedni kontekst. Zweryfikowałem, że ta metoda działa poprawnie, więc nie umieszczę tutaj kodu.

  • Cały ten kod znajduje się w singletonie, a w metodzie INIT Singletona dodaję słuchacz dla "nsmanagedobjectcontextdidsavenotification" i wywołuje metodę mergeChangesFromContextDidSaveNotification: method

.

// merge changes from the context did save notification to the main context
- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification
{
    NSThread *currentThread = [NSThread currentThread];

    if ([currentThread.name isEqual:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]) {

        // merge changes to the primary context, and wait for the action to complete on the main thread
        [_managedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

        // on the main thread fetch all new data and call the completion block
        dispatch_async(dispatch_get_main_queue(), ^{

            // get objects from the database
            NSMutableArray *objects = [[NSMutableArray alloc] init];
            for (id objectID in savedObjectIDs) {
                NSError *error;
                id object = [_managedObjectContext existingObjectWithID:objectID error:&error];
                if (error) {
                    [self logError:error];
                } else if (object) {
                    [objects addObject:object];
                }
            }

            // remove all saved object IDs from the array
            [savedObjectIDs removeAllObjects];
            savedObjectClass = nil;

            // call the completion block
            //completion(objects);
            saveCompletionBlock(objects);

            // clear the saved completion block
            saveCompletionBlock = nil;
        });
    }
}

Jak widać w powyższej metodzie nazywam "mergeChangesFromContextDidSaveNotification:" w głównym wątku, a ja ustawiłem akcję czekać do końca. Zgodnie z dokumentacją apple wątek w tle powinien poczekać, aż ta akcja zostanie zakończona, zanim będzie kontynuowana z resztą kodu poniżej tego wywołania. Jak wspomniałem powyżej po uruchomieniu tego kodu wszystko wydaje się działać, ale kiedy próbuję wydrukować pobrane obiekty na konsolę, nic nie odzyskuję. Wydaje się, że połączenie w rzeczywistości nie ma miejsca, a może nie kończę zanim reszta mojego kodu uruchomi się. Czy jest jeszcze jedno powiadomienie, którego powinienem słuchać, aby upewnić się, że połączenie zostało zakończone? Czy muszę zapisać kontekst głównego obiektu po scaleniu, ale przed fecth?

Również przepraszam za złe formatowanie kodu, ale wygląda na to, że znaczniki kodu SO nie lubią definicji metod.

Dzięki chłopaki!

Aktualizacja:

Wprowadziłem zmiany, które zostały zalecane poniżej, ale nadal mam ten sam problem. Poniżej znajduje się zaktualizowany kod, który mam.

Jest to kod, który wywołuje procesy zapisywania wątków w tle

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    if (savedObjectIDs.count > 0) {
        [savedObjectIDs removeAllObjects];
    }
    if (savedObjectClass) {
        savedObjectClass = nil;
    }

    // set the thead name
    NSThread *currentThread = [NSThread currentThread];
    [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

    // if there is not already a background context, then create one
    if (!_backgroundQueueManagedObjectContext) {
        NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
        if (coordinator != nil) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    // save the JSON dictionary starting at the upper most level of the key path
    NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (objectIds) {
        [savedObjectIDs addObjectsFromArray:objectIds];
    }
    if (completion) {
        saveCompletionBlock = completion;
    }
    if (managedObjectClass) {
        savedObjectClass = managedObjectClass;
    }

    // listen for the merge changes from context did save notification
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    // save all changes object context
    [self saveManagedObjectContext];
});

Jest to kod, który jest wywoływany przez powiadomienie NSManagedObjectContextDidSaveNotification

    // merge changes from the context did save notification to the main context
- (void)mergeChangesFromBackground:(NSNotification *)notification
{
    // kill the listener
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    NSThread *currentThread = [NSThread currentThread];

    // merge changes to the primary context, and wait for the action to complete on the main thread
    [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

    // dispatch the completion block
    dispatch_async(dispatch_get_main_queue(), ^{

        // get objects from the database
        NSMutableArray *objects = [[NSMutableArray alloc] init];
        for (id objectID in savedObjectIDs) {
            NSError *error;
            id object = [[self managedObjectContext] existingObjectWithID:objectID error:&error];
            if (error) {
                [self logError:error];
            } else if (object) {
                [objects addObject:object];
            }
        }

        // remove all saved object IDs from the array
        [savedObjectIDs removeAllObjects];
        savedObjectClass = nil;

        // call the completion block
        //completion(objects);
        saveCompletionBlock(objects);

        // clear the saved completion block
        saveCompletionBlock = nil;
    });
}

Aktualizacja:

Więc znalazłem rozwiązanie. Okazuje się, że sposób, w jaki zapisywałem identyfikatory obiektów w wątku tła, a następnie próbowałem ich użyć w głównym wątku, aby je ponownie pobrać, nie działał. Więc skończyło się na ciągnięciu wstawione / zaktualizowane obiekty ze słownika userInfo, który jest wysyłany z powiadomieniem NSManagedObjectContextDidSaveNotification. Poniżej znajduje się mój zaktualizowany kod, który teraz działa.

Tak jak wcześniej ten kod rozpoczyna pre-prossesing i zapisywanie logiki

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    // set the thead name
    NSThread *currentThread = [NSThread currentThread];
    [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

    [self logMessage:[NSString stringWithFormat:@"(%@) saveJSONObjects:objectMapping:class:completion:", [managedObjectClass description]]];

    // if there is not already a background context, then create one
    if (!_backgroundQueueManagedObjectContext) {
        NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
        if (coordinator != nil) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    // save the JSON dictionary starting at the upper most level of the key path
    [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (completion) {
        saveCompletionBlock = completion;
    }

    // listen for the merge changes from context did save notification
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    // save all changes object context
    [self saveManagedObjectContext];
});

Jest to zmodyfikowana metoda, która obsługuje NSManagedObjectContextDidSaveNotification

- (void)mergeChangesFromBackground:(NSNotification *)notification
{
    // kill the listener
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    // merge changes to the primary context, and wait for the action to complete on the main thread
    [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

    // dispatch the completion block
    dispatch_async(dispatch_get_main_queue(), ^{

        // pull the objects that were saved from the notification so we can get them on the main thread MOC
        NSDictionary *userInfo = [notification userInfo];
        NSMutableArray *modifiedObjects = [[NSMutableArray alloc] init];
        NSSet *insertedObject = (NSSet *)[userInfo objectForKey:@"inserted"];
        NSSet *updatedObject = (NSSet *)[userInfo objectForKey:@"updated"];

        if (insertedObject && insertedObject.count > 0) {
            [modifiedObjects addObjectsFromArray:[insertedObject allObjects]];
        }
        if (updatedObject && updatedObject.count > 0) {
            [modifiedObjects addObjectsFromArray:[updatedObject allObjects]];
        }

        NSMutableArray *objects = [[NSMutableArray alloc] init];

        // iterate through the updated objects and find them in the main thread MOC
        for (NSManagedObject *object in modifiedObjects) {
            NSError *error;
            NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error];
            if (error) {
                [self logError:error];
            }
            if (obj) {
                [objects addObject:obj];
            }
        }

        modifiedObjects = nil;

        // call the completion block
        saveCompletionBlock(objects);

        // clear the saved completion block
        saveCompletionBlock = nil;
    });
}
Author: pkamb, 2012-10-02

3 answers

W Twoim przypadku, ponieważ pisanie do tła moc powiadomienie o mergeChangesFromContextDidSaveNotification pojawi się w tle moc, a nie na pierwszym planie moc.

Więc musisz zarejestrować się, aby otrzymywać powiadomienia w wątku tła przychodzącym do obiektu moc tła.

Po otrzymaniu tego połączenia możesz wysłać wiadomość do głównego wątku moc do mergeChangesFromContextDidSaveNotification.

Andrzej

Update: oto próbka, która powinno działać

    //register for this on the background thread
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self selector:@selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:backgroundMOC];

- (void)mergeChanges:(NSNotification *)notification {
    NSManagedObjectContext *mainThreadMOC = [singleton managedObjectContext];

    //this tells the main thread moc to run on the main thread, and merge in the changes there
    [mainThreadMOC performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
}
 4
Author: andrew lattis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-10-02 15:34:04

Wyrzucę to tam. przestań stosować najlepsze praktyki dotyczące współbieżności wymienione w Przewodniku programowania podstawowych danych . Apple nie zaktualizowało go od czasu dodania zagnieżdżonych kontekstów, które są znacznie łatwiejsze w użyciu. Ten film jest pełen szczegółów: https://developer.apple.com/videos/wwdc/2012/?id=214

Skonfiguruj główny kontekst, aby używał głównego wątku (odpowiedni do obsługi interfejsu użytkownika):

NSManagedObjectContext * context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context setPersistentStoreCoordinator:yourPSC];

Dla dowolnego obiektu, który tworzysz, który może wykonywać jednocześnie operacje, Utwórz prywatny kontekst kolejki do użycia

NSManagedObjectContext * backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[backgroundContext setParentContext:context];
//Use backgroundContext to insert/update...
//Then just save the context, it will automatically sync to your primary context
[backgroundContext save:nil];

QueueConcurrencyType odnosi się do kolejki, w której kontekst będzie wykonywał operacje fetch (Zapisz i pobierz żądanie). Kontekst NSMainQueueConcurrencyType wykonuje całą swoją pracę w kolejce głównej, co sprawia, że jest odpowiedni do interakcji z interfejsem użytkownika. Nsprivatequeueconcurrencytype robi to w swojej prywatnej kolejce. Więc kiedy wywołujesz save na backgroundContext, Scala to prywatne dane wywołując parentContext używając performBlock jako odpowiednie automatycznie. Nie chcesz wywoĹ 'ywaÄ ‡ performBlock w kontekĹ" cie kolejki prywatnej na wypadek, gdyby znajdowaĹ 'a siÄ ™ w gĹ' Ăłwnym wątku, ktĂłry spowoduje zablokowanie.

Jeśli chcesz być naprawdę elegancki, możesz utworzyć kontekst podstawowy jako typ współbieżności kolejki prywatnej (który jest odpowiedni do zapisywania w tle) z kontekstem kolejki głównej dla twojego interfejsu użytkownika, a następnie konteksty podrzędne kontekstu kolejki głównej dla operacji w tle (np. import).

 25
Author: Fruity Geek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-01-25 17:15:55

Widzę, że wypracowałeś odpowiedź, która Ci odpowiada. Ale miałem kilka podobnych problemów i chciałem podzielić się moim doświadczeniem i zobaczyć, czy jest to w ogóle pomocne dla Ciebie lub innych patrząc na tę sytuację.

Wielowątkowe dane rdzeniowe są zawsze trochę mylące, więc proszę mi wybaczyć, jeśli źle odczytałem Twój kod. Ale wydaje się, że może być prostsza odpowiedź dla Ciebie.

Główny problem, który wystąpił podczas pierwszej próby, to zapisanie identyfikatorów zarządzanych obiektów (rzekomo identyfikatory obiektów, które mogą być przekazywane między wątkami) do zmiennej globalnej do użycia w głównym wątku. Zrobiłeś to w tle. Problem polegał na tym, że zrobiłeś to przed zapisaniem do kontekstu managed object w wątku tła. Identyfikatory obiektów nie mogą być bezpiecznie przekazywane do innej pary wątku/kontekstu przed zapisem. Mogą się zmienić po zapisaniu. Zobacz ostrzeżenie w dokumentacji objectID: nsmanagedobject reference

Naprawiłeś to powiadamiając wątek w tle zapisywania, a wewnątrz tego wątku pobiera identyfikatory obiektów teraz-bezpieczne-w-użyciu-ponieważ-kontekst-został-zapisany z obiektu powiadomień. Zostały one przekazane do głównego wątku, a rzeczywiste zmiany zostały również połączone w główny wątek z wezwaniem do mergeChangesFromContextDidSaveNotification. Tutaj możesz zapisać krok lub dwa.

Rejestrujesz się, aby usłyszeć NSManagedObjectContextDidSaveNotification w wątku w tle. Ty może zarejestrować się, aby usłyszeć to samo powiadomienie w głównym wątku . I w tym powiadomieniu będziesz mieć te same identyfikatory obiektów, które są bezpieczne do użycia w głównym wątku. Główny wątek MOC może być bezpiecznie aktualizowany za pomocą mergeChangesFromContextDidSaveNotification i przekazanego obiektu notification, ponieważ metoda jest zaprojektowana do działania w ten sposób: mergeChanges docs . Wywołanie bloku zakończenia z dowolnego wątku jest teraz bezpieczne, o ile dopasujesz moc do wątku blok zakończenia jest wywoływany.

Więc można zrobić wszystkie główne wątku aktualizacji rzeczy na głównym wątku, czyste oddzielenie wątków i unikanie konieczności pakowania i przepakowywania zaktualizowanych rzeczy lub robi podwójne zapisywanie tych samych zmian w trwałym sklepie.

Aby było jasne - Scalanie, które się dzieje, znajduje się w managed object context i jego stanie w pamięci - moc w głównym wątku jest aktualizowana tak, aby pasowała do wątku w tle, ale nowy zapis nie jest konieczny, ponieważ Już zapisane te zmiany w sklepie w wątku tła. Wątek ma bezpieczny dostęp do każdego z tych zaktualizowanych obiektów w obiekcie powiadomień, tak jak wtedy, gdy był używany w wątku tła.

Mam nadzieję, że Twoje rozwiązanie działa dla Ciebie i nie musisz ponownie uwzględniać-ale chciałem dodać moje myśli dla innych, którzy mogą to zobaczyć. Proszę dać mi znać, jeśli źle zinterpretowałem Twój kod, a zmienię.

 7
Author: giff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-01-25 16:43:40