Smart Home
Smart Home

Reputation: 811

CoreData and Concurrency: Unexplained behavior

I heard a lot about issues with CoreData and concurrency. Hence, I decided to try out some scenarios using dummy code. I am not able to fully explain all the observations. Any pointers would be greatly appreciated.

Case 1 The same managed objects are being changed continuously in two different places, main thread and a background thread using the code below. Managed object content save is not executed.

Observation: No crashes. I see that the value of "numberOfSales" is different between that read by the "main thread" and in the "background queue". They eventually become the same, diverge, become the same etc. So, I am guessing this is "eventual consistency" exhibiting itself.

Is this expected behavior? I.e it seems ok to make changes to objects in the same managed object context from multiple threads.

Case 2 The two pieces of code also execute a save of managed object context to persistent store

Observation: Random crashes. Does this mean real issue is when you try to store things to persistent store from multiple threads?

Case 3 I serialize the fetch-requests by using a serial queue. Shown in code sample 2 below.

Observation: No crashes. But I was expecting the fetch requests to serially: one from main thread and one from the background Q. But I see that only one of them executes. Why is this happening?

Block of code executed in background Q

dispatch_async(backgroundQueue, ^(void) {
            while (1) {
                sleep(1);
                NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
                NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person"
                                                          inManagedObjectContext:self.managedObjectContext];
                [fetchRequest setEntity:entity];
                NSError *error;
                NSArray *fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
                for (Person *info in fetchedObjects) {
                    NSLog(@"BackQ Name: %@, SSN: %@, Num sales = %@", info.name,info.ssn, info.numberOfSales);
                    info.numberOfSales = @(2);
                }

             //In case 1: The save to persistent store part below is commented out, in case 2: this part exists

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

Block of code executed in main thread

while (1) {
        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
        NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person"
                                                  inManagedObjectContext:self.managedObjectContext];
        [fetchRequest setEntity:entity];
        [fetchRequest setPredicate:[NSPredicate predicateWithFormat:@"(name = %@)",@"XXX"]] ;
        NSError *error;
        NSArray <Person *>*fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
        fetchedObjects[0].numberOfSales = @([fetchedObjects[0].numberOfSales integerValue] + 1);
        NSLog(@"Fore Name: %@, SSN: %@, Num sales = %@", fetchedObjects[0].name,fetchedObjects[0].ssn, fetchedObjects[0].numberOfSales);

        //In case 1: The save to persistent store part below is commented out, in case 2: this part exists

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

Code Sample 2

   self.coreDataQ = dispatch_queue_create("com.smarthome.coredata.bgqueue2", DISPATCH_QUEUE_SERIAL);

Code Sample 2: Code in Background Q

        dispatch_async(backgroundQueue, ^(void) {
                while (1) {
                    dispatch_async(self.coreDataQ, ^(void) {
                        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
                        NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person"
                                                                  inManagedObjectContext:self.managedObjectContext];
                        [fetchRequest setEntity:entity];
                        NSError *error;
                        NSArray *fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
                        for (Person *info in fetchedObjects) {
                            NSLog(@"BackQ Name: %@, SSN: %@, Num sales = %@", info.name,info.ssn, info.numberOfSales);
                            info.numberOfSales = @(2);
                        }
                        if (![self.managedObjectContext save:&error]) {
                            NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
                        }
                    });
                }
        });

Code Sample 2: Code in Main thread

   while (1) {
        dispatch_async(self.coreDataQ, ^(void) {
            NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
            NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person"
                                                      inManagedObjectContext:self.managedObjectContext];
            [fetchRequest setEntity:entity];
            [fetchRequest setPredicate:[NSPredicate predicateWithFormat:@"(name = %@)",@"XXX"]] ;
            NSError *error;
            NSArray <Person *>*fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
            fetchedObjects[0].numberOfSales = @([fetchedObjects[0].numberOfSales integerValue] + 1);
            NSLog(@"Fore Name: %@, SSN: %@, Num sales = %@", fetchedObjects[0].name,fetchedObjects[0].ssn, fetchedObjects[0].numberOfSales);
            if (![self.managedObjectContext save:&error]) {
                NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
            }
        });
    }

Upvotes: 0

Views: 30

Answers (1)

Tom Harrington
Tom Harrington

Reputation: 70946

If you're using dispatch_async for concurrent Core Data code, you're already doing it wrong. The fact that it doesn't crash immediately doesn't mean anything; you've gone past the signs that say "Warning, killer dragons ahead", and the fact that no dragon has eaten you yet doesn't mean you're doing something safe.

If you're using Core Data on more than one thread or queue, you must use either performBlock or performBlockAndWait for every operation that touches Core Data in any way. This implies that you created your managed object contexts using either NSMainQueueConcurrencyType or NSPrivateQueueConcurrencyType. There is only one exception to this rule: If you used NSMainQueueConcurrencyType and you are certain that your code is running from the main queue, you don't have to use performBlock or performBlockAndWait.

Analyzing the flow in your sample code is not useful; you are severely violating Core Data's concurrency rules, so the only explanation that really matters is that it's inconsistent because you're doing it wrong.

Upvotes: 2

Related Questions