神刀安全网

Solution for CoreData Multithreading problem.

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

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

Solution for CoreData Multithreading problem.

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

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

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.

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.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Solution for CoreData Multithreading problem.

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址