New thread + NSManagedObjectContext - ios

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

Related

Core Data, Concurrency and GCD

I'm having difficulties trying to figure out how to use Core Data concurrently and properly.
I have to clear Core Data of an entity before adding new data every time there is an update. Therefore I've decided to use this snippet:
-(void)addSale:(NSArray *)results{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *entity = #"Sale";
CoreDataManager.sharedInstance.delegate = self;
dispatch_async(dispatch_get_main_queue(), ^{
[CoreDataManager.sharedInstance deleteEntityWithName:entity];
});
NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
privateContext.parentContext = CoreDataManager.sharedInstance.managedObjectContext;
for (NSDictionary *dataDictionary in [results valueForKey:#"Sales"])
{
NSManagedObject *managedObject = [NSEntityDescription insertNewObjectForEntityForName:entity inManagedObjectContext:privateContext];
// Fill ManagedObject
// .....
}
NSError *error;
[privateContext save:&error];
if (error != nil) {
NSLog(#"Couldn't save private context bcoz of %#\n%#", error, error.localizedDescription);
}
dispatch_async(dispatch_get_main_queue(), ^{
// Save Main ManagedObjectContext
[CoreDataManager.sharedInstance saveContext:CoreDataManager.sharedInstance.managedObjectContext WithEntityName:entity];
});
});
}
Problem is that I have to do the same thing for 2 other entities while keeping the UI responsive without having any impacts on the Core Data.
Is there any better approaches to handle this?
Create a private context associated with the NSPersistentStoreCoordinator.
Delete objects in the private context.
Save private context.
Tell main queue context to reset if any of those objects were being used.
There really is no reason to delete objects on the main context (which it appears you are doing with your singleton).
If your UI has not touched any of the objects you are deleting then you do not need to reset the context associated with the User Interface.
You could also, instead of doing a reset, have the main queue context consume the save notification from the private queue context and that will net the same result.

Multiple NSManagedObjectContexts or single context and -performBlock

I have been using Core Data with a single NSManagedObjectContext for a long time, all fetching, saving, background update operations will be done on single context through helper classes, I was planning to implement a multiple NSManagedObjectContext approach (which is the recommended solution in most of my searching).
My question is: is performBlock the only was to execute code for that context? Can't we do something like below:
- (void) checkSyncServer {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
//do something here, check and fetch data
// create NSManagedObject's
[_tempContext save:&error];
//masterContext will merge changes through notification observers
});
}
(i.e) execute code apart from -performBlock method. How can I execute multiple asynchronous methods and perform a save?
However, I find a single context (which is managed by one singleton NSObject class) simpler to use.
This multiple context with ContextConcurrencyType looks more complicated (in terms of execution flow). Is there a better solution?
You can access contexts in one of two ways:
On its Thread/Queue. This applies to confined contexts and main queue contexts. You can access them freely from their own thread.
With -performBlock: if it is a private queue context or if you are touching the context from a thread other than the one it belongs on.
You cannot use dispatch_async to access a context. If you want the action to be asynchronous then you need to use -performBlock:.
If you were using a single context before and you were touching it with a dispatch_async you were violating the thread confinement rule.
Update
When you call [[NSManagedObjectContext alloc] init] that is functionally equivalent to [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType].
The NSManagedObjectContext has always been thread confined.
As for executing multiple methods you can just call them all in the same block:
NSManagedObjectContext *moc = ...;
[moc performBlock:^{
//Fetch something
//Process data
//Save
}];
Or you could nest them if you wanted them to be all async of each other:
NSManagedObjectContext *moc = ...;
[moc performBlock:^{
//Fetch Something
[moc performBlock:^{
//Process Data
}];
[moc performBlock:^{
//Save
}];
}];
Since -performBlock: is re-entrant safe you can nest them all you want.
Update Async save
To do an async save you should have two contexts (or more):
Main Queue context that the UI talks to
Private Queue context that saves
Private context has a NSPersistentStoreCoordinator and the main queue context has the private as its parent.
All work is done in the main queue context and you can save it safely, normally on the main thread. That save will be instantaneous. Afterwards, you do an async save:
NSManagedObjectContext *privateMOC = ...;
NSManagedObjectContext *mainMOC = ...;
//Do something on the mainMOC
NSError *error = nil;
if (![mainMOC save:&error]) {
NSLog(#"Main MOC save failed: %#\n%#", [error localizedDescription], [error userInfo]);
abort();
}
[privateMOC performBlock:^{
NSError *error = nil;
if (![privateMOC save:&error]) {
NSLog(#"Private moc failed to write to disk: %#\n%#", [error localizedDescription], [error userInfo]);
abort();
}
}];
If you already have an app, all you need to do is:
Create your private moc
Set it as the parent of your main
Change your main's init
Add the private block save method whenever you call save on your main
You can refactor from there but that is all you really need to change.

Core Data deadlocking when executing fetch requests inside performBlockAndWait blocks

I am experiencing issues with Core Data which I cannot resolve. I've learned about concurrency issues in Core Data the hard way, thus I am really careful and only perform any core data operations in performBlock: and performBlockAndWait: blocks.
Here goes my code:
/// Executes a fetch request with given parameters in context's block.
+ (NSArray *)executeFetchRequestWithEntityName:(NSString *)entityName
predicate:(NSPredicate *)predicate
fetchLimit:(NSUInteger)fetchLimit
sortDescriptor:(NSSortDescriptor *)sortDescriptor
inContext:(NSManagedObjectContext *)context{
NSCAssert(entityName.length > 0,
#"entityName parameter in executeFetchRequestWithEntityName:predicate:fetchLimit:sortDescriptor:inContext:\
is invalid");
__block NSArray * results = nil;
NSPredicate * newPredicate = [CWFCoreDataUtilities currentUserPredicateInContext:context];
if (predicate){
newPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:#[newPredicate, predicate]];
}
[context performBlockAndWait:^{
NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:entityName];
request.fetchLimit = fetchLimit;
request.predicate = newPredicate;
if (sortDescriptor) {
request.sortDescriptors = #[sortDescriptor];
}
NSError * error = nil;
results = [context executeFetchRequest:request error:&error];
if (error){
#throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:#"Fetch requests are required to succeed."
userInfo:#{#"error":error}];
NSLog(#"ERROR! %#", error);
}
NSCAssert(results != nil, #"Fetch requests must succeed");
}];
return results;
}
When I enter this method concurrently from two different threads and pass two different contexts, I result in a deadlock on this row: results = [context executeFetchRequest:request error:&error];
Which is interesting: it seems like both threads cannot acquire some lock on the Persistent Store Coordinator in order to execute a fetch request.
All of my contexts are NSPrivateQueueConcurrencyType.
I can't put my finger on, why am I locking the app and what should I do differently. My research on Stack Overflow gave me nothing, since most of the people fixed all the locks by dispatching the fetch requests on the MOC's queue, which I already do.
I will appreciate any information on this issue. Feel free to provide documentation links and other long reads: I am eager to learn more about all kind of concurrency problems and strategies.
Which is interesting: it seems like both threads cannot acquire some lock on the Persistent Store Coordinator in order to execute a fetch request.
The persistent store coordinator is a serial queue. If one context is accessing it another context will be blocked.
From Apple Docs:
Coordinators do the opposite of providing for concurrency—they serialize operations. If you want to use multiple threads for different write operations you use multiple coordinators. Note that if multiple threads work directly with a coordinator, they need to lock and unlock it explicitly.
If you need to perform multiple background fetch request concurrently you will need multiple persistent store coordinators.
Multiple coordinators will make your code only slightly more complex but should be avoided if possible. Do you really need to do multiple fetches at the same time? Could you do one larger fetch and then filter the in-memory results?
If you are interested in learning more about Core Data (and threading), the following website will be extremely helpful. I attended Matthew Morey's talk at the Atlanta CocoaConf 2013.
High Performance Core Data (http://highperformancecoredata.com)
The companion sample code to the website is available at: https://github.com/mmorey/MDMHPCoreData
All you need to do in your app, is to have a Singleton instance (somewhere) of the MDMPersistenceStack class.
As far as your problem/issue, even though the Apple documentation for NSManagedObjectContext class, does allow for code to be written the way that you have (with the Core Data operations performed on an NSManagedObjectContext instance in a block) and to some extent encourages that - I would like to point out that is not the only way.
Whenever I have fixed apps that have Core Data concurrency issues (locking), the simplest thing, in my opinion, is to create a private NSManagedObjectContext inside the thread or block that I want to execute Core Data operations on.
It may not be the most elegant approach, but it's never failed me. One is always guaranteed that the NSManagedObjectContext is created and executed in the same thread, because the NSManagedObjectContext is explicitly created.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
/*
Create NSManagedObjectContext
Concurrency of Managed Object Context should be set to NSPrivateQueueConcurrencyType
If you use the MDMPersistenceStack class this is handled for you.
*/
NSManagedObjectContext *managedObjectContext = ....
/*
Call the method that you have listed in your code.
Let's assume that this class method is in MyClass
Remove the block that you have in your method, as it's not needed
*/
[MyClass executeFetchRequestWithEntityName: ......] // rest of parameters
});
I did it this way. It fixed the issue for me. I was experiencing a lot of deadlocks too. See if it works for you.
+ (NSArray *)getRecordsForFetchRequest:(NSFetchRequest *)request inContext:(NSManagedObjectContext *)context
{
#try
{
__weak __block NSError *error = nil;
__block __weak NSArray *results = nil;
[context performBlockAndWait:^{
[context lock];
results = [context executeFetchRequest:request error:&error];
[context processPendingChanges];
[context unlock];
}];
[self handleErrors:error];
request = nil;
context = nil;
return results;
}
#catch (NSException *exception)
{
if([exception.description rangeOfString:#"Can only use -performBlockAndWait: on an NSManagedObjectContext that was created with a queue"].location!=NSNotFound)
{
NSError *error = nil;
[context lock];
__weak NSArray *results = [context executeFetchRequest:request error:&error];
[context processPendingChanges];
[context unlock];
[self handleErrors:error];
request = nil;
context = nil;
return results;
}
return nil;
}
}

iOS background save in Core Data

I want to ensure that my main thread never blocks, that's why I want to do my Core Data saves in the background.
I've been reading the Apple docs together with this link (and many others, but I found this one pretty useful): http://www.cocoanetics.com/2012/07/multi-context-coredata/, though I cannot get the architecture right.
In my AppDelegate.m:
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_saveContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_saveContext setPersistentStoreCoordinator:coordinator];
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:_saveContext];
}
return _managedObjectContext;
}
Then to save, I would do something like this:
// var 'context' is the context coming from method managedObjectContext
// this code is in the same thread 'context' is created in (the main thread)
NSError *error = nil;
if ([context save:&error]) {
[context.parentContext performBlock:^{
NSError *err = nil;
if(![context.parentContext save:&err]) {
NSLog(#"Error while saving context to the persistent store");
}
}];
} else {
// handle error
}
This is what I would get from reading the link I supplied earlier. Saving does not work, once the app is closed and reopened, the changes made to any managed objects are gone: they were never saved to the persisted store.
Makes sense I guess since I created 2 NSManagedObjectContexts in 1 thread, Apple docs clearly state to have only 1 NSManagedObjectContext per thread. So how do I setup the parent/child relation between _managedObjectContext and _saveContext? I know _saveContext needs to be initialised in another thread, but I cannot get this approach to work.
(From the comments)
All the "new" managed object context types (NSMainQueueConcurrencyType, NSPrivateQueueConcurrencyType) manage their own threads, it is not necessary to create the context on a special thread. The only thing to remember is always to use performBlock or performBlockAndWait for operations in the context. This ensures that the operations are executed on the right queue and thread.
So your code is OK.
(As it turned out, the error was that a wrong context was passed to your saving routine and therefore the inner save was not done in the top-level context.)

Saving Core Data using AlertView and immediately in Cell

When I push a cell, an AlertView with Prompt is popping up. My problem: I want to show the entered text from the prompt in to the selected cell. (and in the meantime save the text to Core Data). Can anyone push me in the right direction ?
You need to do the save in a background thread, if you want it to happen at the same time as the alert is showing.
The easiest approach is using nested contexts, and just saving from the main context.
Wherever you are creating your managed object context, replace the alloc/init part with...
NSManagedObjectContext *parentMoc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
parentMoc.persistentStoreCoordinator = persistentStoreCoordinator;
self.managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
self.managedObjectContext.parentContext = parenetMoc;
Now, you have the same MOC you were using before, except it is a main queue MOC, with a parent context running in a background queue.
You will have to use a method to save both contexts though. The second save, on the parent, happens in a background thread, so you do not have to wait.
- (void)saveData {
NSError *error = nil;
NSManagedObjectContext *moc = self.managedObjectContext;
if ([moc save:&error]) {
moc = moc.parentContext;
[moc performBlock:^{
NSError *error = nil;
if (![moc save:&error]) {
// Handle the actual save error
}
}];
} else {
// Handle the error of saving up into the parent context...
}
}
Now, instead of calling [managedObjectContext save:&error] directly, replace it with a message of saveData, and the method will return almost immediately, and the actual save will happen in a background thread.
None of your other code in your app (except for the save calls) should have to change at all.
In your case, right before you throw up the alert, call save, and the save will happen while the alert is being displayed.

Resources