Solution for CoreData Multithreading problem.

By | May 1, 2016

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”.

CoreData Multithreading iOS

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..

  1. You have to create a parent NSManagedObjectContext which is tied to the persistent store coordinator and is running on the main thread
  2. You can create a child NSManagedObjectContext which runs in a separate background thread and can be connected to the parent context.
  3. When saving a child NSManagedObjectContext this is done in memory to the parent context.
  4. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *