Reputation: 1417
I have a Core Data stack with a main managed object context with NSMainQueueConcurrencyType.
The user can initiate a task on a managed object that can take a long time, so it is performed on a separate context:
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:mainMOC];
Person *samePerson = (Person *)[context objectWithID:person.objectID];
[context performBlock:^{
// BLOCK 1
// do lots of work
// then update the managed object
samePerson.value = someCalculatedValue;
// save the private context
NSError *error;
if (![context save:&error]) {
NSLog(@"Error: %@", error);
}
[mainMOC performBlock:^{
NSError *error;
if (![mainMOC save:&error]) {
NSLog(@"Error saving: %@", error);
}
}];
}];
This works fine, and the main MOC gets updated properly, NSFetchedResultsController hooked up to it perform properly, etc.
The problem is with deleting. I have this setup to delete objects:
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:mainMOC];
[context performBlock:^{
// BLOCK 2
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:@"Person"];
NSError *error;
NSArray *all = [context executeFetchRequest:request error:&error];
if (!all) {
NSLog(@"Error fetching: %@", error);
} else {
for (NSManagedObject *person in all) {
[context deleteObject:person];
}
NSError *error;
if (![context save:&error]) {
NSLog(@"Error saving: %@", error);
}
[mainMOC performBlock:^{
NSError *error;
if (![mainMOC save:&error]) {
NSLog(@"Error saving: %@", error);
}
}];
}
}];
Now if I do this delete operation (Block 2) during the time it takes to perform the long-duration task (Block 1), then the delete operation finishes quickly, and saves to main context. After a while Block 1 finishes, and when it saves the mainMOC at its end, we get a seemingly obvious crash:
CoreData could not fulfill a fault for ...
My question is: how do I perform a task such as Block 1 with the possibility of its object being deleted?
Upvotes: 1
Views: 226
Reputation: 2257
If these are both background tasks that cannot run at the same time, try using semaphores to protect access to them.
eg. for an instance variable:
dispatch_semaphore_t _backgroundProcessingSemaphore;
Lazily initialised using something like:
- (dispatch_semaphore_t)backgroundProcessingSemaphore
{
if (!_backgroundProcessingSemaphore) {
_backgroundProcessingSemaphore = dispatch_semaphore_create(1);
}
return _backgroundProcessingSemaphore;
}
Surround the critical code with:
dispatch_semaphore_wait(self.backgroundProcessingSemaphore, DISPATCH_TIME_FOREVER);
// Critical code
dispatch_semaphore_signal(self.backgroundProcessingSemaphore);
Only one critical section of code can then run at any point in time. The block that calls dispatch_semaphore_wait will block if the semaphore is already taken, until it is freed up.
You also probably want to think about splitting your long-duration task up so that it will run in discrete batches if you're not already doing so - this is useful if the long running background task timer is about to expire while you still have work to do - you can stop and restart from the appropriate point on next launch.
Other options would involve forcing a save on block 1 before block 2 saves itself, but this starts to get messy. Much easier to ensure the two competing blocks cannot overlap.
Upvotes: 1