Reputation: 811
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
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