This post is for people who are at least familiar with CoreData.
But just knowing is not everything..
CoreData creates problems when you access them from different threads…
Most common problems are listed below
-
CoreData: error: Serious application error.
Exception was caught during Core Data change processing.
This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification.
-[__NSCFSet addObject:]: attempt to insert nil with userInfo (null) -
Terminating app due to uncaught exception ‘NSGenericException’,
reason: ‘*** Collection was mutated while being enumerated.’ -
Terminating app due to uncaught exception ‘NSInternalInconsistencyException’,
reason: ‘recordChangeSnapshot:forObjectID:: global ID may not be temporary when recording
Now We will See what is the reason for these errors/crashes and how we can solve it.
Reason
The Main reason why the above crashes happen is that you are accessing the same CoreData ManagedObjectContext from different threads.
The new Core Data functionality is based upon the principle of thread confinement: each NSManagedObjectContext is tight to one and only one thread. When performing an operation on a NSManagedObjectContext (reading or writing) you have to make sure that this is done on the correct thread.
There are actually more than one kinds of solution
Solution 1
We will first check the first solution.
I will just do a demo to simulate a crash and then propose the solution.
Below is a coreData Entity named “Person” with two attribues “name” and “age”.
I will simply add and read data from the table from different threads.
// Below function adds the data to the database. -(void) addData { AppDelegate *apppDel = (AppDelegate *)[[UIApplication sharedApplication] delegate]; NSManagedObjectContext *context = apppDel.managedObjectContext; // Create a new managed object NSManagedObject *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:context]; [person setValue:@"Coderzheaven" forKey:@"name"]; [person setValue:@28 forKey:@"age"]; NSError *error = nil; // Save the object to persistent store if ([context hasChanges] && ![context save:&error]) { NSLog(@"Can't Save! %@ %@", error, [error localizedDescription]); }else{ NSLog(@"Saved"); } } // Read the data from CoreData.. -(void) readData { // Fetch the devices from persistent data store AppDelegate *apppDel = (AppDelegate *)[[UIApplication sharedApplication] delegate]; NSManagedObjectContext *context = apppDel.managedObjectContext; NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"Person"]; self.tblVales = [[context executeFetchRequest:fetchRequest error:nil] mutableCopy]; NSLog(@"Rows %d", (int) self.tblVales.count); }
Now I will call these methods from the viewDidLoad..
- (void)viewDidLoad { [super viewDidLoad]; // Adding record from Main Thread [self addData]; for(int i = 0 ; i < 10; i ++){ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // Adding and reading from separate thread [self addData]; [self readData]; if(i % 2 == 0){ // Reading Record from Main Thread dispatch_async(dispatch_get_main_queue(), ^{ [self readData]; }); } }); } // Reading from Main Thread for(int i = 0 ; i < 10; i ++){ [self readData]; } }
Now try to run this application, You are going to encounter any of the issues I showed above.
Now How will we solve this.
Create Concurrency…
See what apple Says.
NSManagedObjectContext now provides structured support for concurrent operations. When you create a managed object context using initWithConcurrencyType:, you have three options for its thread (queue) association
Confinement (NSConfinementConcurrencyType).
This is the default. You promise that context will not be used by any thread other than the one on which you created it. (This is exactly the same threading requirement that you’ve used in previous releases.)
Private queue (NSPrivateQueueConcurrencyType).
The context creates and manages a private queue. Instead of you creating and managing a thread or queue with which a context is associated, here the context owns the queue and manages all the details for you (provided that you use the block-based methods as described below).
Main queue (NSMainQueueConcurrencyType).The context is associated with the main queue, and as such is tied into the application’s event loop, but it is otherwise similar to a private queue-based context. You use this queue type for contexts linked to controllers and UI objects that are required to be used only on the main thread.
So we will also follow this..
Solution 1
While allocating your Main Managed Object, add “NSMainQueueConcurrencyType” to our Main CoreData ManagedObjectContext.
If it is a separate thread, add “NSPrivateQueueConcurrencyType” for the ManagedObjectContext.
Let’s see how the implementation goes…
- (NSManagedObjectContext *)managedObjectContext { // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) if (_managedObjectContext != nil) { return _managedObjectContext; } NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (!coordinator) { return nil; } _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; [_managedObjectContext setPersistentStoreCoordinator:coordinator]; return _managedObjectContext; }
But this alone will not solve the problem..
You need to add all core data operations inside performBlock or performBlockAndWait.
So our addData and readData functions will change like this.
-(void) addData { AppDelegate *apppDel = (AppDelegate *)[[UIApplication sharedApplication] delegate]; NSManagedObjectContext *context = apppDel.managedObjectContext; __block NSError *error = nil; [context performBlock:^{ // Create a new managed object NSManagedObject *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:context]; [person setValue:@"CoderzHeaven" forKey:@"name"]; [person setValue:@25 forKey:@"age"]; NSError *error = nil; // Save the object to persistent store if ([context hasChanges] && ![context save:&error]) { NSLog(@"Can't Save! %@ %@", error, [error localizedDescription]); }else{ NSLog(@"Saved"); } }]; if (error) { // handle the error. NSLog(@"ERRRR %@", error.localizedDescription); } } -(void) readData { // Fetch the devices from persistent data store AppDelegate *apppDel = (AppDelegate *)[[UIApplication sharedApplication] delegate]; NSManagedObjectContext *context = apppDel.managedObjectContext; //[apppDel getNewContext]; __block NSError *error = nil; [context performBlock:^{ NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"Person"]; self.tblVales = [[context executeFetchRequest:fetchRequest error:&error] mutableCopy]; NSLog(@"Rows %d", (int) self.tblVales.count); }]; }
you could simply change performBlock to performBlockAndWait
performBlockAndWait is Synchronous.
performBlock is asynchronous.
You could use either of them depending upon your logic
Solution 2
We will write a separate function for getting the correct context in current thread. If there is no context, we will create a new context and save to Current Thread’s Dictionary and later retrive it when the same thread is reading or writing to CoreData. It can be Main Thread
or any other Thread at any time.
// Get the new context if the DB context is on a different thread... -(NSManagedObjectContext *) getCurrentContext { NSManagedObjectContext *curMOC = [self managedObjectContext]; NSThread *thisThread = [NSThread currentThread]; if(thisThread == [NSThread mainThread]){ if (curMOC != nil) { return curMOC; } NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { curMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; [curMOC setPersistentStoreCoordinator:coordinator]; } return curMOC; } // if this is some other thread.... // Get the current context from the same thread.. NSManagedObjectContext *_threadManagedObjectContext = [[thisThread threadDictionary] objectForKey:@"MOC_KEY"]; // Return separate MOC for each new thread if (_threadManagedObjectContext != nil) { return _threadManagedObjectContext; } NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { _threadManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; [_threadManagedObjectContext setPersistentStoreCoordinator: coordinator]; [[thisThread threadDictionary] setObject:_threadManagedObjectContext forKey:@"MOC_KEY"]; } return _threadManagedObjectContext; } -(void) saveThreadContext :(NSManagedObjectContext *) context { NSManagedObjectContext *managedObjectContext = context; [managedObjectContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy]; if (managedObjectContext != nil) { [managedObjectContext performBlock:^{ NSError *error = nil; if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) { NSLog(@"BG CONTEXT Unresolved error %@, %@", error, [error userInfo]); }else{ NSLog(@"Context Saved"); } }]; } }
you could change the addData and readData like this…
-(void) addData { AppDelegate *apppDel = (AppDelegate *)[[UIApplication sharedApplication] delegate]; NSManagedObjectContext *context = [apppDel getCurrentContext]; NSError *error = nil; // Create a new managed object NSManagedObject *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:context]; [person setValue:@"CoderzHeaven" forKey:@"name"]; [person setValue:@25 forKey:@"age"]; // Save the object to persistent store if ([context hasChanges] && ![context save:&error]) { NSLog(@"Can't Save! %@ %@", error, [error localizedDescription]); }else{ NSLog(@"Saved"); } if (error) { // handle the error. NSLog(@"ERRRR %@", error.localizedDescription); } } -(void) readData { // Fetch the devices from persistent data store AppDelegate *apppDel = (AppDelegate *)[[UIApplication sharedApplication] delegate]; NSManagedObjectContext *context = [apppDel getCurrentContext]; NSError *error = nil; NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"Person"]; self.tblVales = [[context executeFetchRequest:fetchRequest error:&error] mutableCopy]; NSLog(@"Rows %d", (int) self.tblVales.count); }
Solution 3
Using ParentContexts.
The idea goes like this..
- You have to create a parent NSManagedObjectContext which is tied to the persistent store coordinator and is running on the main thread
- You can create a child NSManagedObjectContext which runs in a separate background thread and can be connected to the parent context.
- When saving a child NSManagedObjectContext this is done in memory to the parent context.
- You can perform ‘blocks’ on every context which are then scheduled for processing.
Create a new context variable in the .h file.
@property (nonatomic, retain) NSManagedObjectContext *threadManagedObjectContext;
We will rewrite our newContext Method
-(NSManagedObjectContext *) getNewContext { NSManagedObjectContext *curMOC = self.managedObjectContext; NSThread *thisThread = [NSThread currentThread]; if(thisThread == [NSThread mainThread]){ if (self.managedObjectContext != nil) { return self.managedObjectContext; } NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { curMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; [curMOC setPersistentStoreCoordinator:coordinator]; } return curMOC; } if (_threadManagedObjectContext != nil) { return _threadManagedObjectContext; } NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { _threadManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; _threadManagedObjectContext.parentContext = self.managedObjectContext; } return _threadManagedObjectContext; }
Now Make changes in our add and delete methods.
You should now perform operations with performBlock using the parentContext.
-(void) addData { AppDelegate *apppDel = (AppDelegate *)[[UIApplication sharedApplication] delegate]; NSManagedObjectContext *context = [apppDel getNewContext]; NSManagedObjectContext *p = context.parentContext; __block NSError *error = nil; //Run using parent context [p performBlockAndWait:^{ // Create a new managed object NSManagedObject *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:context]; [person setValue:@"CoderzHeaven" forKey:@"name"]; [person setValue:@25 forKey:@"age"]; // Save the object to persistent store if ([context hasChanges] && ![context save:&error]) { NSLog(@"Can't Save! %@ %@", error, [error localizedDescription]); }else{ NSLog(@"Saved"); } if ([p hasChanges] && ![p save:&error]) { NSLog(@"Can't Save! %@ %@", error, [error localizedDescription]); }else{ NSLog(@"Saved"); } }]; if (error) { // handle the error. NSLog(@"ERRRR %@", error.localizedDescription); } } -(void) readData { // Fetch the devices from persistent data store AppDelegate *apppDel = (AppDelegate *)[[UIApplication sharedApplication] delegate]; NSManagedObjectContext *context = [apppDel getNewContext]; __block NSError *error = nil; [context performBlock:^{ NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"Person"]; self.tblVales = [[context executeFetchRequest:fetchRequest error:&error] mutableCopy]; NSLog(@"Rows %d", (int) self.tblVales.count); }]; }
All Done.
Thankyou.
Send your valuable comments to coderzheaven@gmail.com.
I’ve came to your article after some google search and is really useful.
I use in my app something like your third solution. I would be grateful if you can tell me if there is a way to keep both contexts synced. If I understand right the parent context doesn’t propagate the changes to its child so if I add data to the main context the “_threadManagedObjectContext” will be out of date.
Thank you in advance and once more congratulations for your article!
When a write is done in the child, it will be automatically propagated to the parent context.
what wrong with my setup
func getmanagedContext() -> NSManagedObjectContext {
var context : NSManagedObjectContext?
//if self.persistentContainer != nil {
if Thread.isMainThread {
context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context?.persistentStoreCoordinator = self.persistentStoreCoordinator
return context!
}
else {
var newcontext = Thread.current.threadDictionary.object(forKey: “MOC_KEY”) as? NSManagedObjectContext
if newcontext != nil{
return newcontext!
}
else {
newcontext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
newcontext?.persistentStoreCoordinator = self.persistentStoreCoordinator
Thread.current.threadDictionary[“MOC_KEY”] = newcontext
return newcontext!
}
}
// }
}
//fetchdata from core data
func getAllPerson() {
let fetchReqest = NSFetchRequest(entityName: “Person”)
let context = appdelgate?.getmanagedContext()
do {
self.persons = (try! context!.fetch(fetchReqest))
}
}
catch let error as NSError {
print(error.localizedDescription)
}
}
it’s working file with perform
func getAllPerson() {
let fetchReqest = NSFetchRequest(entityName: “Person”)
let context = appdelgate?.getmanagedContext()
do {
context?.perform {
self.persons = (try! context!.fetch(fetchReqest))
// self.personTableView.reloadData()
}
}
catch let error as NSError {
print(error.localizedDescription)
}
}
What is the error you are getting?
I have enable -com.apple.CoreData.ConcurrencyDebug 1 option.when I access getAllPerson from background like
DispatchQueue.global(qos: .background).async {
self.getAllPerson()
}
I get following error
/Users/bhavesh_mac/Desktop/Screen Shot 2018-02-24 at 6.31.45 PM.png
#0 0x000000010d69e3f4 in +[NSManagedObjectContext __Multithreading_Violation_AllThatIsLeftToUsIsHonor__] ()
Do all your 3 solutions listed are suitable to avoid UI freeze when core data operations are performed in background.
yes exactly.