Core Data Concurrency Import Performance - ios

I’m working on an app connected to a web service which retrieves lot of data during app launch. I use concurrency to avoid UI blocking. I choosed the following Core Data Stack pattern : background private moc —> main moc —> writer private moc —> coordinator —> file.
The problem occures when operations are being imported. The CPU is 100% used and the app gets slow along the process. I work with batches of 300 objects for a total import of about 10,000 objects.
For each batch, an NSOperation is created with an associated temporary moc, child of the background one. Operation is enqueue in an NSOperationQueue.
When the importing jobs are done, the app get even slower, depending on the number of jobs running. I also note that when the app is killed, and relaunched, it’s really way more usable and fast.
My memory footprint changes between 40Mo and 60Mo when importing. Do you think it’s too much?
Do you think my stack pattern is appropriate for my needs? Should I migrate to a stack with 2 coordinators?
Moreover, when fetching data to display in tableView, should I use performBlockAndWait to get data immediately before displaying the view ?
Thanks for your help

Your stack as described is fine.
CPU usage can be misleading. You want to make sure you are not on the main thread as that will cause most of your slowness and/or stuttering in the app.
When you watch your app in Instruments, what is taking the most time? How much time is spent on the main queue?
In general, imports shouldn't be causing the CPU to sit at 100%. If you are doing that from a background thread there is most likely some performance gains to be made.
If should share your import code and or Instruments trace so that I can see what is going on.

I think your setup is problematic. You state that the child of the background managed object context is main thread and that you create such children to import. This is bound to cause UI glitches.
Also, I believe that relying on NSOperation is unnecessary over-engineering. You should use the NSManagedObjectContext block APIs instead.
My recommended setup would be:
RootContext (background, writing to persistent store) -> parent of
MainContext (foreground, UI) -> parent of
WorkerContext(s) (background, created and discarded ad hoc)
You can create worker contexts in the callbacks of your web calls to do the heavy lifting for the import. Make sure you are using the block APIs and confine all objects to the local context. You save the context to push the changes up to the main thread (which can already start displaying data before it is saved to the store), and periodically, you save the main context and the writer context, always using the bock APIs.
A typical such saveContext function that can be called thread safe (here self refers to the data manager singleton or app delegate):
func saveContext () {
if self.managedObjectContext.hasChanges {
self.managedObjectContext.performBlocAndWait {
do { try self.managedObjectContext.save() }
catch let error as NSError {
print("Unresolved error while saving main context \(error), \(error.userInfo)")
}
}
self.rootContext.performBlockAndWait {
do { try self.rootContext.save() }
catch let error as NSError {
print("Unresolved error while saving to persistent store \(error), \(error.userInfo)")
}
}
}
}

After few days of test and instruments trace, I can give you more details. The following snippet shows how I save my context (based on parent/child pattern) from a shared instance :
- (void)save {
[self.backgroundManagedObjectContext performBlockAndWait:^{
[self saveContext:self.backgroundManagedObjectContext];
[self.mainManagedObjectContext performBlock:^{
[self saveContext:self.mainManagedObjectContext];
[self.writerManagedObjectContext performBlock:^{
[self saveContext:self.writerManagedObjectContext];
}];
}];
}];
}
- (void)saveContext:(NSManagedObjectContext*)context {
NSError *error = nil;
if ([context hasChanges] && ![context save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
}
Then each import job is perform on the background context thank to a synchronous operation. The following method is fired in operation main.
- (void)operationDidStart
{
NSManagedObjectContext *moc = self.context;
NSMutableArray *insertedOrUpdatedObjects = [NSMutableArray array];
NSMutableArray *subJSONs = [NSMutableArray array];
NSUInteger numberOfJobs = ceil((double)self.JSONToImport.count/self.batchSize);
for (int i = 0; i < numberOfJobs; i++) {
NSUInteger startIndex = i * self.batchSize;
NSUInteger count = MIN(self.JSONToImport.count - startIndex, self.batchSize);
NSArray *arrayRange = [self.JSONToImport subarrayWithRange:NSMakeRange(startIndex, count)];
[subJSONs addObject:arrayRange];
}
__block NSUInteger numberOfEndedJobs = 0;
for (NSArray *subJSON in subJSONs) {
[moc performBlock:^{
[self startJobWithJSON:subJSON context:moc completion:^(NSArray *importedObjects, NSError *error) {
numberOfEndedJobs++;
if (!error && importedObjects && importedObjects.count > 0) {
[insertedOrUpdatedObjects addObjectsFromArray:importedObjects];
}
if (numberOfEndedJobs == numberOfJobs) {
[[CoreDataManager manager] save];
if (self.operationCompletion) {
self.operationCompletion(self, insertedOrUpdatedObjects, error);
}
}
}];
}];
}
}
As you can see, I segment my import in batches (of 500). The operation perform each batch on the background context queue and I save my stack when all batches are ended.
It seems the save method take 23% of CPU usage for each thread thanks to Time Profiler.
Hope to be as clear as possible.

Related

Core Data Save Error on iOS 10: Unresolved error Error Domain=NSCocoaErrorDomain Code=133020

I've been testing the new core data stack in iOS 10. My test app parses JSON data into core data and I am trying to make this happen while the user has access to the UI.
I am using the default core data stack and using a background context.
In AppDelegate.m:
- (NSPersistentContainer *)persistentContainer {
// The persistent container for the application. This implementation creates and returns a container, having loaded the store for the application to it.
#synchronized (self) {
if (_persistentContainer == nil) {
_persistentContainer = [[NSPersistentContainer alloc] initWithName:#"CoreDataTestingMDC"];
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
if (error != nil) {
NSLog(#"Unresolved error %#, %#", error, error.userInfo);
abort();
}
}];
}
}
_persistentContainer.viewContext.automaticallyMergesChangesFromParent = YES;
return _persistentContainer;
}
I have a simple master-detail UI that shows the core data entities in the master view controller and detailed attributes in the detail view. If the user does not scroll the master view, everything works fine. If the user scrolls, I usually get this error on save:
Unresolved error Error Domain=NSCocoaErrorDomain Code=133020 "(null)" UserInfo={conflictList=(
"NSMergeConflict (0x600000667c00) for NSManagedObject (0x610000096490) with objectID '0xd000000000440000 <x-coredata://CFF27A51-8F9E-4898-A4EA-CD85C0AFF300/ContentItem/p17>'
with oldVersion = 44 and newVersion = 45...
It goes on to list the conflicting items which have exactly the same properties.
Also in my AppDelegate, I added a simple convenience method to generate the background context:
- (NSManagedObjectContext *)createBackgroundContext {
return [self.persistentContainer newBackgroundContext];
}
This is passed back to the AppDelegate for a save operation:
- (void)saveContext:(NSManagedObjectContext *) theContext {
NSError *error = nil;
if ([theContext hasChanges] && ![theContext save:&error]) {
NSLog(#"Unresolved error %#, %#", error, error.userInfo);
abort();
}
}
The UI is running on the viewContext as expected. I have been very careful to use the background context for all JSON parser writing. No idea why this is crashing.
Update:
It appears that the error occurs whenever the app is run after an initial run. I can test it fine on a clean simulator or after deleting the app. It parses data into core data fine and will also update while the user is interacting with the app. On a second build and run, the app will crash with the above error.
It looks to me that you are having multiple writes taking place concurrently. To solve this you need write to core data in a single synchronous way.
In your core-data manager create a NSOperationQueue
_persistentContainerQueue = [[NSOperationQueue alloc] init];
_persistentContainerQueue.maxConcurrentOperationCount = 1;
And do all writing using this queue:
- (void)enqueueCoreDataBlock:(void (^)(NSManagedObjectContext* context))block{
void (^blockCopy)(NSManagedObjectContext*) = [block copy];
[self.persistentContainerQueue addOperation:[NSBlockOperation blockOperationWithBlock:^{
NSManagedObjectContext* context = self.persistentContainer.newBackgroundContext;
[context performBlockAndWait:^{
blockCopy(context);
[context save:NULL]; //Don't just pass NULL here. look at the error and log it to your analytics service
}];
}]];
}
When you call enqueueCoreDataBlock the block is enqueued to ensures that there are no merge conflicts. But if you write to the viewContext that would defeat this setup. Likewise you should treat any other contexts that you create (with newBackgroundContext or with performBackgroundTask) as readonly because they will also be outside of the writing queue.
At first I thought that NSPersistentContainer's performBackgroundTask had an internal queue, and initial testing supported that. After more testing I saw that it could also lead to merge conflicts.
Also facing the same issue. But i solved it using the MergePolicy of ManagedObjectContext. By default the merge policy is NSMERGEPOLICYERROR. By changing it to NSMergeByPropertyObjectTrumpMergePolicy fixes the NSManagedObject Conflict issues for me.
Check which merge policy suites your requirement.

Core Data: SQLite not being updated immediately

There are two Entities- Document and Page. Document has a one-to-many relationship with Page.
I save the managed object context when I add document. At this point, there are no pages in them. While debugging I found that the writer context's save method does get called and is executed without error. I close and reopen the app and I can't find the previously saved Document objects. But, if I add a page in one of the document, then, the Document object appear in the table. I use a tool to view the SQLite file but my observation is not based on what I see in the tool. Even when I debug and see the number of documents present, I get 0 back when there is no page in them.
I am guessing that the Persistent Store Coordinator is doing some kind of optimization to write in batch. Can I force it to write and update the persistent store immediately? Is there a option that I can add while calling addPersistentStoreWithType on the persistent store object?
Note: Just FYI, I use this pattern to organize the Managed Object Context(s)
Fixed the issue. Here is the update
So, I was saving the whole stack all the way up to the writer context. The bug was very silly. I was trying to save the main context on the main thread like this:
- (void)saveMainContext {
[self.mainManagedObjectContext performBlock:^{
// Ensure that the main object context is being saved on the main queue
__block NSError *error = nil;
dispatch_async(dispatch_get_main_queue(), ^{
[self.mainManagedObjectContext save:&error];
});
if(!error)
{
//Write to disk after saving on the main UI context
[self saveWriterContext];
}
}];
}
As you can see, after trying to save the main context, I save the writer context. But, the bug was that I wasn't waiting for the main context to finish saving. After fixing the bug, my code looks like this:
- (void)saveMainContext {
[self.mainManagedObjectContext performBlock:^{
// Ensure that the main object context is being saved on the main queue
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error = nil;
[self.mainManagedObjectContext save:&error];
if(!error)
{
//Write to disk after saving on the main UI context
[self saveWriterContext];
}
});
}];
}
And, this fixed the issue! Very silly mistake on my part.
Are you making sure you are saving your entire stack? If you make a change in a private context you need to save that private context. If you make a change in the main context (from the UI) then you need to save that context. Only after all of your other contexts report NO to -hasChanges should you save the writer context (aka the master context in his design).
I suspect that is your issue.
Response to OP
Hmm. Did not know that. Thanks! So, are you suggesting that I may be well off if I do not check for "error" at all, and just check for the save's return?
What I am saying is that your save should look like this (note I also correct your unnecessary dispatch_async):
- (void)saveMainContext {
[self.mainManagedObjectContext performBlock:^{
// Ensure that the main object context is being saved on the main queue
NSError *error = nil;
if (![[self mainManagedObjectContext] save:&error]) {
NSLog("Failed to save context: %#\n%#", [error localizedDescription], [error userInfo]);
exit(1);
}
[self saveWriterContext];
}];
}
The dispatch_async will be ignored because you are already on the right queue.
The call to -save: returns a bool. If and ONLY if that returns NO do you react to the error.

MagicalRecord: saving context after deleting is too long

I have next problem: I have database with about 7000 of entities, when I need to update them (I have XML file which I parse) for first I delete all entities, after it I parse XML file, later I create new entities and save context. Earlier all worked perfect: no freezes, no crashes - all was fast on iOS 7.
But with release iOS 8 there were problems:
I resolved this problem by providing one context for all operations: deleting, creating and saving.
BUT! What I've got:
When I just install app on my device all goes well: there are no deleting, only creating entities, 7000 terms and 7 groups are parsed so fast (about 4 seconds on iPhone 6), saving goes fast too.
When I changing version of DB in my plist file (increase) my parser start this algorthm:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
[Term MR_truncateAllInContext:localContext];
[Group MR_truncateAllInContext:localContext];
} completion:^(BOOL success, NSError *error) {
[self parseTermsInContext:[NSManagedObjectContext contextForCurrentThread] from:self.count];
}];
});
"saveWithBlock" method blocks Thread 1 (in profiler), my CPU loaded on 99-108(error apparently) percents (with every next update saving operation takes more and more seconds, from 20 and more, more than 120 seconds).
I've tried this way (I gathered all operations in one method for you):
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSManagedObjectContext *localContext = [NSManagedObjectContext contextForCurrentThread];
NSMutableArray *objects = [NSMutableArray arrayWithArray:[Group MR_findAllInContext:localContext]];
[objects addObjectsFromArray:[Term MR_findAllInContext:localContext]];
if (objects && [objects count] > 0) {
for (NSManagedObject *object in objects) {
[object MR_deleteInContext:localContext];
}
[localContext MR_saveToPersistentStoreAndWait];
}
[self parseTermsInContext:localContext from:self.count];
});
Here operation "MR_saveToPersistentStoreAndWait" take a long time too like "saveWithBlock".
I tried way without saving context after deleting, that is line "[localContext MR_saveToPersistentStoreAndWait];" does not exists. In this way Groups and Terms was deleted so fast too, later they was parsed so fast too but saving context was so long.
And I don't know why but even if I start deleting and saving processes in background thread saving operation froze UI thread (in UI thread I show progress from 0 to 100). When I parse XML in this thread I send message to view that one term is parsed and setting progress in percents, delegate calls method for setting progress in ProgressView in main queue.
I have not another threads that can operate core data objects.
There is link with work of app: http://rghost.ru/60274051
After 6 seconds: for test purposes I start NSTimer that updated progress every 0.3 second with fake data to fill progress for 50% before starting deleting and saving operation (updating progress goes in main queue). Timer fires several times then saving process starts in background thread but blocks main thread (as I understand) and moves setting progress operation to end (if I understand correctly).
1:08 : then after saving ends I start parsing xml-file. This is thread where I saved context after deleting. You can see progress updating. In this video it works with bugs because of a lot of manipulations, but You can believe me that it works and looks fine. After parsing 7000 objects I save context AGAIN and saving operation does not block UI thread.
Additional info:
Relations:
} completion:^(BOOL success, NSError *error) {
[self parseTermsInContext:[NSManagedObjectContext contextForCurrentThread] from:self.count];
}];
block above called in Main Thread (block UI), so contextForCurrentThread == UI Thread
instead of iterate an delete all objects you can use TruncateAll
My suggestion is:
// saveWithBlock - already perform block in background thread
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
[Term MR_truncateAllInContext:localContext];
[Group MR_truncateAllInContext:localContext];
} completion:^(BOOL success, NSError *error) {
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
// self must be weak!
[self parseTermsInContext:localContext from:self.count];
} completion:^(BOOL success, NSError *error) {
// Update UI
}];
}];

New thread + NSManagedObjectContext

I'm trying to separate my application work when there is a bigger work to do to optimize performance. My problem is about a NSManagedObjectContext used in another thread than the main one.
I'm calling:
[NSThread detachNewThreadSelector:#selector(test:) toTarget:self withObject:myObject];
On the test method there are some stuff to do and I have a problem here:
NSArray *fetchResults = [moc
executeFetchRequest:request
error:&error];
Here is my test method:
-(void) test:(MyObject *)myObject{
#autoreleasepool {
//Mycode
}
}
The second time I call the test method, my new thread is blocked when the executeFetchRequest is called.
This problem arrived when my test method is called more than one time in succession. I think the problem comes from the moc but I can't really understand why.
Edit:
With #Charlie's method it's almost working. Here is my code to save my NSManagedObjectContext (object created on my new thread).
- (void) saveContext:(NSManagedObjectContext *) moc{
NSError *error = nil;
if ([moc hasChanges] && ![moc save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
}
This method is called on the new thread. My problem now is that with this save, I have a deadlock and I don't really understand why. Without it's perfectly working.
Edit2
I'm working on this issue but I still can't fix it. I changed my code about the detachNewThreadSelector. Here is my new code:
NSManagedObjectContext* context = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
context.persistentStoreCoordinator = self.persistentStoreCoordinator;
context.undoManager = nil;
[context performBlock:^
{
CCImages* cachedImage;
NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
childContext.parentContext = context;
cachedImage=[CCImages getCCImageForKey:path inManagedObjectContext:childContext];
UIImage *image = [self getImageFromCacheWithPath:path andCachedImage:cachedImage atDate:now];
if (image != nil){
if(![weakSelf.delegate respondsToSelector:#selector(CacheCacheDidLoadImageFromCache:)])
[weakSelf setDelegate:appDelegate.callbacksCollector];
//[weakSelf useCallbackCollectorForDelegate:weakSelf inMethod:#"initPaginatorForListMoments"];
[weakSelf.delegate CacheCacheDidLoadImageFromCache:image];
}
}
- (UIImage*) getImageFromCacheWithPath:(NSString*) path andCachedImage:(CCImages *) cachedImage atDate: (NSDate *) now{
NSURL* localURL=[NSURL URLWithString:cachedImage.path relativeToURL:[self imageCacheDirectory]];
UIImage * image;
//restore uiimage from local file system
if (localURL) {
image=[UIImage imageWithContentsOfFile:[localURL path]];
//update cache
[cachedImage setLastAccessedAt:now];
[self saveContext];
if(image)
return image;
}
return nil;
}
Just after that, I'm saving my contexts (manually for now)
[childContext performBlock:^{
NSError *error = nil;
if (![childContext save:&error]) {
DDLogError(#"Error during context saving when getting image from cache : %#",[error description]);
}
else{
[context performBlock:^{
NSError *error = nil;
if (![context save:&error]) {
DDLogError(#"Error during context saving when getting image from cache : %#",[error description]);
}
}];
}
}];
There is a strange problem. My call back method is called without any problem on my controller (which implements the CacheCacheDidLoadImageFromCache: method). On this method I attest the reception of the image (DDLogInfo) and say that I want my spinner to stop. It does not directly but only 15secondes after the callback method was called.
My main problem is that my context (I guess) is still loading my image from the cache while it was already found. I said 'already' because the callback method has been called and the image was present. There is no suspicious activity of the CPU or of the memory. Instruments didn't find any leak.
I'm pretty sure that I'm using wrongly the NSManagedObjectContext but I can't find where.
You are using the old concurrency model of thread confinement, and violating it's rules (as described in the Core Data Concurrency Guide, which has not been updated yet for queue confinement). Specifically, you are trying to use an NSManagedObjectContext or NSManagedObject between multiple threads.
This is bad.
Thread confinement should not be used for new code, only to maintain the compatibility of old code while it's being migrated to queue confinement. This does not seem to apply to you.
To use queue confinement to solve your problem, first you should create a context attached to your persistent store coordinator. This will serve as the parent for all other contexts:
+ (NSManagedObjectContent *) parentContextWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)coordinator {
NSManagedObjectContext *result = nil;
result = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[result setPersistentStoreCoordinator:coordinator];
return result;
}
Next, you want the ability to create child managed object contexts. You will use these to perform work on the data, wether reading or writing. An NSManagedObjectContext is a scratchpad of the work you are doing. You can think of it as a transaction. For example, if you're updating the store from a detail view controller you would create a new child context. Or if you were performing a multi-step import of a large data set, you would create a child for each step.
This will create a new child context from a parent:
+ (NSManagedObjectContext *) childContextWithParent:(NSManagedObjectContext *)parent {
NSManagedObjectContext *result = nil;
result = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[result setParent:parent];
return result;
}
Now you have a parent context, and you can create child contexts to perform work. To perform work on a context, you must wrap that work in performBlock: to execute it on the context's queue. I do not recommend using performBlockAndWait:. That is intended only for re-rentrant methods, and does not provide an autorelease pool or processing of user events (user events are what drives nearly all of Core Data, so they're important. performBlockAndWait: is an easy way to introduce bugs).
Instead of performBlockAndWait: for your example above, create a method that takes a block to process the results of your fetch. The fetch, and the block, will run from the context's queue - the threading is done for you by Core Data:
- (void) doThingWithFetchResults:(void (^)(NSArray *results, NSError *error))resultsHandler{
if (resultsHandler != nil){
[[self context] performBlock:^{
NSArray *fetchResults = [[self context] executeFetchRequest:request error:&error];
resultsHandler(fetchResults, error);
}];
}
}
Which you would call like this:
[self doThingsWithFetchResults:^(NSArray *something, NSError *error){
if ([something count] > 0){
// Do stuff with your array of managed objects
} else {
// Handle the error
}
}];
That said, always prefer using an NSFetchedResultsController over using executeFetch:. There seems to be a belief that NSFetchedResultsController is for powering table views or that it can only be used from the main thread or queue. This is not true. A fetched results controller can be used with a private queue context as shown above, it does not require a main queue context. The delegate callbacks the fetched results controller emits will come from whatever queue it's context is using, so UIKit calls need to be made on the main queue inside your delegate method implementations. The one issue with using a fetched results controller this way is that caching does not work due to a bug.
Again, always prefer the higher level NSFetchedResultsController to executeFetch:.
When you save a context using queue confinement you are only saving that context, and the save will push the changes in that context to it's parent. To save to the store you must recursively save all the way. This is easy to do. Save the current context, then call save on the parent as well. Doing this recursively will save all the way to the store - the context that has no parent context.
Example:
- (void) saveContextAllTheWayBaby:(NSManagedObjectContext *)context {
[context performBlock:^{
NSError *error = nil;
if (![context save:&error]){
// Handle the error appropriately.
} else {
[self saveContextAllTheWayBaby:[context parentContext]];
}
}];
}
You do not, and should not, use merge notifications and mergeChangesFromContextDidSaveNotification: with queue confinement. mergeChangesFromContextDidSaveNotification: is a mechanism for the thread confinement model that is replaced by the parent-child context model. Using it can cause a whole slew of problems.
Following the examples above you should be able to abandon thread confinement and all of the issues that come with it. The problems you are seeing with your current implementation are only the tip of the iceberg.
There are a number of Core Data sessions from the past several years of WWDC that may also be of help. The 2012 WWDC Session "Core Data Best Practices" should be of particular interest.
if you want to use managed object context in background thread, there are two approaches,
1 Create a new context set concurrency type to NSPrivateQueueConcurrencyType and set the parentContext to main thread context
2 Create a new context set concurrency type to NSPrivateQueueConcurrencyType and set persistentStoreCoordinator to main thread persistentStoreCoordinator
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
privateContext.persistentStoreCoordinator = mainManagedObjectContext.persistentStoreCoordinator;
[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {
NSManagedObjectContext *moc = mainManagedObjectContext;
if (note.object != moc) {
[moc mergeChangesFromContextDidSaveNotification:note];
}
}];
// do work here
// remember managed object is not thread save, so you need to reload the object in private context
});
before exist the thread, make sure remove the observer, bad thing can happen if you don't
for more details read http://www.objc.io/issue-2/common-background-practices.html

Deadlock when saving context with concurrency type NSPrivateQueueConcurrencyType

Since two days I'm trying to get Core Data to work with multiple threads. I tried standard thread confinement method with NSOperations, merging notifications, using objectWithId, dictionaries of contexts per thread and still I get strange deadlocks, inconsistency exceptions and a bunch of other nasty stuff. It's driving me crazy... moreover I can't find a single example or explanation on how to manage context in two threads when both threads may make changes to the shared persistent store...
I tried to use new iOS 5 method, that supposed to be easier, but still I get errors. The first problem is the deadlock when saving context. I removed all the unnecessary code and stil get deadlocks when executing this code fast enough (by quickly tapping a button):
NSManagedObjectContext *context = [StoreDataRetriever sharedRetriever].managedObjectContext;
for (int i = 0; i < 5; i++) {
NSError *error = nil;
NSLog(#"Main thread: %#, is main? %d", [NSThread currentThread], [NSThread isMainThread]);
BOOL saveOK = [context save:&error];
if (!saveOK) {
NSLog(#"ERROR!!! SAVING CONTEXT IN MAIN");
}
[context performBlock:^{
NSLog(#"Block thread: %#", [NSThread currentThread]);
NSError *error = nil;
BOOL savedOK = NO;
savedOK = [context save:&error];
if (!savedOK) {
NSLog(#"ERROR!!! SAVING CONTEXT IN BLOCK");
}
}];
}
There are no other changes to the database, nothing, only saving context. What is wrong with this code? How should it look like?
Note: [StoreDataRetriever sharedRetriever].managedObjectContext is created in appDelegate using initWithConcurrencyType:NSPrivateQueueConcurrencyType.
What's going on with that code? You are saving the context on a thread synchronously, then you schedule a save on the context private queue. 5 times. So basically, you may well have two save operations, one synchronous and one asynchronous, colliding with each other.
This is clearly an issue. You aren't supposed to save a context with a private queue outside of that queue. It will work with the current context implementation provided there is no scheduled block on the context queue. But this is wrong nevertheless.
…
for (int i = 0; i < 5; i++) {
NSLog(#"Main thread: %#, is main? %d", [NSThread currentThread], [NSThread isMainThread]);
__block NSError *error = nil;
__block BOOL saveOK = YES;
[context performBlockAndWait: ^{
saveOK = [context save: &error];
}];
if (!saveOK) {
NSLog(#"ERROR!!!");
}
…
With that code, you execute the save operation synchronously and most certainly on the same thread - thanks GCD - sparing context switches and synchronization stuff, and without any risk of having two operations running on that context at the same time.
The same rule applies when using NSMainQueueConcurrencyType, with an exception. That queue is bound to the main thread and the main thread only. You can schedule blocks on a context using the main queue from any thread with performBlock and performBlockAndWait like NSPrivateQueueConcurrencyType, and (the exception:) you can use the context directly on the main thread.
NSConfinementConcurrencyType binds the context to a specific thread and you cannot use GCD or blocks to deal with such a context, only the bound thread. There is very little reasons to use that concurrency model as of today. If you have to, use it, but if you do not absolutely have to, don't.
edit
Here is a very nice article about multi-contextes setups: http://www.cocoanetics.com/2012/07/multi-context-coredata/

Resources