Search code examples
iosobjective-cmultithreadingthread-safetynsmanagedobjectcontext

NSManagedObjectControllers read only access on another thread


I am selecting some Core Data entries and sorting them. This is straight forward enough but because of the calculations in the sort by distance I wanted to put it in an alternate thread. I did that the usual way by making a new NSManagedObjectController for the other thread by adding the code within the #ifdef THREADS sections below.

Now I can do my fetch, then my sorting and organizing, without slowing the user interface and the table will be updated when ready.

But, since I am doing my fetch in the local managed object context, my fetched objects are not valid on my main thread - in fact they disappear out of my table view cells.

So, my questions are: 1) would it be OK to use the main managed object context, since I am only reading, and not create a local context at all? 2) should I go through and re-fetch all the objects from the main managed object context on the main thread using the ObjectID values from the search managed object context objects after sorting and organizing? 3) should I do my fetch on the main managed object context with PerformBlockAndWait but then keep the sort and organize with the returned objects on the searchQ thread? 4) something else I have not thought of?

- (void) fetchShopLocations {
#ifdef THREADS
    dispatch_queue_t searchQ = dispatch_queue_create( "searchQueue", NULL );
    dispatch_async( searchQ, ^{
#endif
        NSError *error;

#ifdef THREADS
        // Make a managed object context that can be not on the main thread
        NSManagedObjectContext *localContext = [[NSManagedObjectContext alloc] init];
        [localContext setPersistentStoreCoordinator: [self.managedObjectContext persistentStoreCoordinator]];
#else
        NSManagedObjectContext *localContext = self.managedObjectContext;
#endif

        // Get the array of locations sorted by distance
        NSArray *locations = NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Shop class])];
        // No sort descriptors since I cannot sort on things not in the database

        NSArray *locations = [context executeFetchRequest: request error: &error];
        if( !locations ) {
            NSLog( @"Shop fetch error: %@", error );
            return;
        }
        locations = [locations sortedArrayUsingComparator: ^NSComparisonResult( Shop *loc1, Shop *loc2 ) {
                    double dist1 = [loc1 distanceFromLatitude: self.referenceLatitude andLongitude: self.referenceLongitude];
                    double dist2 = [loc2 distanceFromLatitude: self.referenceLatitude andLongitude: self.referenceLongitude];
                    if( dist1 < dist2 )
                            return NSOrderedAscending;
                    else if( dist1 > dist2 )
                            return NSOrderedDescending;
                    return [[loc1 name] localizedCaseInsensitiveCompare: [loc2 name]];
        }];

        NSMutableArray *sections = [NSMutableArray arrayWithCapacity: 5];
        NSMutableDictionary *section;
        NSMutableArray *rows;

        section = [NSMutableDictionary dictionaryWithCapacity: 2];
        rows = [NSMutableArray arrayWithCapacity: locations.count];
        __block double sectionLimitUserUnits = 5.0;
        __block double sectionLimitKilometers;
        if( self.userUnits == LocationDistanceUnitsMiles )
              sectionLimitKilometers = sectionLimitUserUnits * KILOMETERS_PER_MILE;
        else  sectionLimitKilometers = sectionLimitUserUnits;
        [section setValue: [NSString stringWithFormat: @"Within %g", sectionLimitUserUnits] forKey: kSectionHeaderTitleKey];
        [locations enumerateObjectsUsingBlock: ^( Shop *loc, NSUInteger idx, BOOL *stop ) {
            double distance = [loc distanceFromLatitude: self.referenceLatitude andLongitude: self.referenceLongitude];
            if( distance > self.maxDistance ) {
                *stop = YES;
            } else {
                while( distance > sectionLimitKilometers ) {
                    [section setValue: [rows copy] forKey: kRowKey];
                    [sections addObject: [section copy]];
                    [section removeAllObjects];
                    [rows removeAllObjects];

                    sectionLimitUserUnits += 5.0;
                    if( self.userUnits == LocationDistanceUnitsMiles )
                          sectionLimitKilometers = sectionLimitUserUnits * KILOMETERS_PER_MILE;
                    else  sectionLimitKilometers = sectionLimitUserUnits;
                    [section setValue: [NSString stringWithFormat: @"Within %g", sectionLimitUserUnits] forKey: kSectionHeaderTitleKey];
                }
                [rows addObject: loc];
            }
        }];
        [section setValue: [rows copy] forKey: kRowKey];
        [sections addObject: [section copy]];

#ifdef THREADS
        dispatch_async( dispatch_get_main_queue(), ^{
#endif
            self.sections = sections;
            [self.tableView reloadData];
#ifdef THREADS
        });
    });
    dispatch_release( searchQ );
#endif
}

Because of the non-deterministic nature of threading conflicts I know I can't rely on just testing what appears to work in the simulator.

Thanks in advance!


Solution

  • 1) would it be OK to use the main managed object context, since I am only reading, and not create a local context at all?

    No. Someday, something will access the main managed object context at the same time with the fetch and it will be a headache. (I have encountered a deadlock doing this.)

    2) should I go through and re-fetch all the objects from the main managed object context on the main thread using the ObjectID values from the search managed object context objects after sorting and organizing?

    Not all the objects, since you are already using a table view, you only need to re-fetch the objects that will be displayed in the table. And re-fetching the objects from the main thread using ObjectID is the correct thing to do.

    3) should I do my fetch on the main managed object context with PerformBlockAndWait but then keep the sort and organize with the returned objects on the searchQ thread

    This is another option, but you need to understand really well how it works. The approach you are doing is actually simpler.

    4) something else I have not thought of?

    You can avoid sorting the array in code by making a query that return the locations ORDER BY distance. See this answer.