Question

I'm having trouble with my app hanging on an NSFetchRequest. I've been reading about this for quite a few hours. There is apparently a threading issue going on, but I can't figure out how to fix it.

I'm downloading a JSON object from my server, parsing it and storing it in a CoreData model. This all occurs on a background thread that is complete before my app hangs. Here is the code where that occurs:

- (void) populateRegistrationList:(NSString *) list script:(NSString *)script
{

    NSURL *URL = [NSURL URLWithString:[PRIMARY_URL stringByAppendingString:script]];
    NSURLRequest *request = [NSURLRequest requestWithURL:URL];
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
    NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request
        completionHandler:^(NSURL *localfile, NSURLResponse *response, NSError *error) {
            if (!error) {

                NSArray *jObj = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfURL:localfile]
                                                                options:kNilOptions
                                                                  error:nil];
                // make sure that the result is successful
                if (![[jObj valueForKey:@"result"] isEqualToString:@"success"]) {
                    return;
                }

                // get a context pointer to the database model
                AppDelegate* appDelegate = [UIApplication sharedApplication].delegate;
                self.managedObjectContext = appDelegate.managedObjectContext;

                // clear out any data from previous downloads
                [self dumpTable:list];

                NSArray *jArr = (NSArray *) [jObj valueForKey:@"message"];

                for (int i=0; i<[jArr count]; i++) {
                    NSDictionary *dict = [jArr objectAtIndex:i];
                    [self commitToCoreData:dict];

                    NSError *error;
                    if (![self.managedObjectContext save:&error])
                        NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
                }

                // set this user default to YES to tell the RegistrationTwoViewController that we are done
                [[NSUserDefaults standardUserDefaults] setBool:YES forKey:REGISTRATION_LISTS_RETRIEVED];
                [[NSUserDefaults standardUserDefaults] synchronize];

            }
        }];

  [task resume];
}

Subsequently, in the main thread, I execute a fetch request to access this data like this:

@implementation ChooseSuperGroupViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    // get a context pointer to the database model
    AppDelegate* appDelegate = [UIApplication sharedApplication].delegate;
    self.managedObjectContext = appDelegate.managedObjectContext;

    NSLog(@"About to get fetched objects");    
    [self.managedObjectContext performBlockAndWait:^{
        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"SuperGroups"];
        NSError *error;
        self.fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
        NSLog(@"Done with fetch request");
    }];
}        

Unfortunately, this hangs on the self.fetchedObjects line. I realize that I am using two different threads, but I know that the background thread has completed before I attempt the fetch request. Why do I have to concern myself with another thread at this point? I don't see how a race condition can occur.

What do I do to fix this? Can I force the fetch request to run on the same background thread somehow? All of the posts and articles that I have read on this seem to be way to complicated or they don't address the threading issue at all. I'm stuck and don't know what else to do.

Any help is appreciated. Thanks.

This is from my AppDelegate:

- (NSManagedObjectContext *) managedObjectContext {
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [_managedObjectContext setPersistentStoreCoordinator: coordinator];
    }

    return _managedObjectContext;
}

- (NSManagedObjectModel *)managedObjectModel {
    if (_managedObjectModel != nil) {
        return _managedObjectModel;
    }
    _managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil];

    return _managedObjectModel;
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }
    NSURL *storeUrl = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory]
                                               stringByAppendingPathComponent: @"PhoneBook.sqlite"]];
    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]
                                   initWithManagedObjectModel:[self managedObjectModel]];
    if(![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
                                                  configuration:nil URL:storeUrl options:nil error:&error]) {
        /*Error for store creation should be handled in here*/
    }

    return _persistentStoreCoordinator;
}
Was it helpful?

Solution

It doesn't really matter that the background thread has completed, the problem is that you can't safely use the same managed object context on multiple threads without ensuring synchronization-- possibly by using GCD calls like dispatch_async but preferably by using Core Data's built-in synchronization system. I don't know specifically why it's failing in this case, but you're violating a major rule of safe Core Data usage, so bad results are expected.

To use Core Data's built-in syncing, create your context using NSMainQueueConcurrencyType or NSPrivateQueueConcurrencyType. Then, whenever you use that context (or objects that you have fetched from that context), put your code inside a performBlock or performBlockAndWait call. This will guarantee synchronous access regardless of the number of threads or queues.

Alternately, create a new managed object context for the background thread. There's no reason to not have more than one. Do your work on this separate context, and listen for NSManagedObjectContextDidSaveNotification on the main thread to merge in new changes.

But whatever you do, don't use the same context on multiple threads without adequate precautions. Even when it works, it's only by chance. It'll fail eventually.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top