dispatch_async never finishes executing entire block - ios

I'm new to GCD, and what seems to be a simple use of it doesn't work for me. I have the following code:
+ (void)synchronizationTimerFired:(NSTimer *)theTimer
{
if ((synchronizationUpNeededFlag) || (synchronizationDownNeededFlag))
{
if ((!synchronizationUpInProgressDepthQuantity) && (!synchronizationDownInProgressDepthQuantity))
{
dispatch_queue_t synchronizationQueue = dispatch_queue_create("synchronizationQueue",NULL);
dispatch_async(synchronizationQueue, ^(void) {
NSLog(#"Top");
...code...
...code...
...code...
NSLog(#"Bottom");
});
}
}
// Check if there is no timer, or if it is not currently valid,
// and yet if synchronization is turned on,
// then establish a repeating timer to attend to synchronization related matters.
if ((!synchronizationTimer) || (!synchronizationTimer.isValid))
{
if (synchronizationOnFlag)
{
synchronizationTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(synchronizationTimerFired:) userInfo:nil repeats:YES];
}
}
}
The log reads "Top" and nothing else. The code in the middle doesn't have an endless loop- it just never executes all the way through. I can put breakpoints in the code in the middle and there's a point where the program execution will break, and after which it won't. And there's a point right in the middle where sometimes the execution will stop at the breakpoint and other times it doesn't.
It seems to me as though the synchronizationQueue dispatch queue is being deallocated, but I can't call dispatch_retain because the compiler complains that dispatch_retain cannot be used in ARC. What am I missing?
In response to people asking about the code in-between, the program execution stops in this method call (represented by one of those lines of ...code...) at the line that says if (fetchArray.count), commented below.
+ (NSDate *)latestParseReceivedDownUpdatedAtDateForCoreDataEntityNameString:(NSString *)coreDataEntityNameString
{
NSDate *functionReturnValue = nil;
// Create fetchRequest
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:coreDataEntityNameString];
// Set sort descriptor
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"parseReceivedDownUpdatedAtDate" ascending:NO]]];
// We are only interested in one result
[fetchRequest setFetchLimit:1];
// Execute fetchRequest
NSError *fetchError = nil;
NSArray *fetchArray = [JBSAPPDELEGATE.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
if (fetchArray == nil)
{
NSLog(#"Unresolved error %#, %#", fetchError, [fetchError userInfo]);
abort();
}
// If there are any records at all in our persistent store, we'll have exactly one.
// But that doesn't mean it won't be nil, as if that record has never come down from
// parse it will be a nil date on the managed object.
if (fetchArray.count) // PROGRAM EXECUTION STOPS EITHER HERE, OR JUST BEFORE HERE
{
NSManagedObject *managedObject = [fetchArray objectAtIndex:0];
functionReturnValue = [managedObject valueForKey:#"parseReceivedDownUpdatedAtDate"];
}
return functionReturnValue;
}
I will add that if I simply comment out the call to dispatch_async that everything executes fine. It just executes on the main thread, which I'd rather have it not do.

Is your managedObjectContext an NSManagedObjectContext? If so, did you create a managed object context on the specific thread that you are using for your dispatch queue? If you read the docs on NSManagedObjectContext, it says:
...a context assumes the default owner is the thread or queue that
allocated it—this is determined by the thread that calls its init
method. You should not, therefore, initialize a context on one thread
then pass it to a different thread. Instead, you should pass a
reference to a persistent store coordinator and have the receiving
thread/queue create a new context derived from that.

I bet you created yourself a deadlock somewhere.
Use the debugger. Check what every thread is doing. You'll probably find some thread that you think should be continuing and isn't.

Related

CoreData asynchronous fetch causes concurrency debugger error

I'm using the
-com.apple.CoreData.ConcurrencyDebug
argument on launch to debug concurrency in my CoreData app.
During app launch, I perform an asynchronous fetch on the main thread's managed object context.
// set up the async request
NSError * error = nil;
[MOC executeRequest:asyncFetch error:&error];
if (error) {
NSLog(#"Unable to execute fetch request.");
NSLog(#"%#, %#", error, error.localizedDescription);
}
This code is called from the main thread, but executeRequest: enqueues it to another thread, which I understand to be the correct behavior.
The concurrency debugger doesn't like this, saying (I reckon) that I'm doing something wrong here. I've also tried wrapping this in [MOC performBlock:] which also works, but also causes a multithreading violation. In both cases I get this :
[NSManagedObjectContext __Multithreading_Violation_AllThatIsLeftToUsIsHonor__
Am I using async fetches incorrectly, or is the concurrency debugger wrong here?
EDIT : I've also tried wrapping it in MOC performBlock which should ensure that it gets called from the main thread. In any case, the call is enqueued from the main thread, but executed elsewhere.
EDIT : here's the fetch request:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"MyEntity"];
NSPredicate * pred = [NSPredicate predicateWithFormat:#"boolProperty == YES"];
fetchRequest.predicate = pred;
NSSortDescriptor * sort = [NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES];
fetchRequest.sortDescriptors = #[sort]; fetchRequest.propertiesToFetch = #[#"prop1", #"prop2", #"prop3", #"prop4"];
NSPersistentStoreAsynchronousFetchResultCompletionBlock resultBlock = ^(NSAsynchronousFetchResult *result) {
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kFetchCompleteNotification object:result];
});
};
NSAsynchronousFetchRequest *asyncFetch = [[NSAsynchronousFetchRequest alloc]
initWithFetchRequest:fetchRequest
completionBlock:resultBlock];
Then I receive the results from the notification:
- (void)fetchCompletedNote:(NSNotification *)note {
NSAsynchronousFetchResult * result = note.object;
if (![cachedResults isEqualToArray:result.finalResult]){
cacheResults = result.finalResult;
[self.collectionView reloadData];
}
}
I think this is Apple's bug.
I filed bug report:
https://openradar.appspot.com/30692722
and added example project that reproduces issue:
https://github.com/jcavar/examples/tree/master/TestAsyncFetchCoreData
Also, if you don't want to disable flag just because of this issue you may want to swizzle __Multithreading_Violation_AllThatIsLeftToUsIsHonor__ method on NSManagedObjectContext just for this part of code.
You need to revert that after request is executed so you get violations for real issues.
The concurrency debugger is telling you that you are accessing MOC from the wrong thread/queue. You can only call -executeRequest: error: on the thread/queue that the context belongs to. If this is a NSMainQueueConcurrencyType then you need to be on the main thread. Otherwise, if it is a NSPrivateQueueConcurrencyType you need to use either -performBlock: or -performBlockAndWait: to run the execute on the correct queue.
I added a screenshot, see above. The request is enqueued from the main thread, but executed on another.
Ok, a couple of things:
Is that the line that is breaking/crashing or are you seeing the error output?
Your error handling is incorrect. You should be looking at the result of the -executeRequest: error: and if the result is nil then you are in an error state. The error variable can be populated even with a success.
I note that the code you posted in your screenshot is different than the code you posted previously. Did you add the -performBlock: or just not include it originally?

Finding NSManagedObject inside of NSOperation

I thought that [moc existingObjectWithID:self.objectID error:&error] was the right way to find an existing object inside of a thread, given its objectID, but I can't get it to work, why?
Background / Code
I've got an NSManagedObject which is already saved. I can see all its properties on the main thread. I'm passing the object to my NSOperation in a custom init method and in that method, I'm saving its objectID:
- (instancetype)initWithEntity:(NSManagedObject *)object
{
self = [super init];
if (self) {
self.objectID = object.objectID;
...
}
return self;
}
Then in the main function of my operation I'm looking up my object so that I can use it.
- (void)main
{
if (self.isCancelled) { return; }
NSManagedObjectContext *moc = [NSManagedObjectContext MR_context];
NSError *error = nil;
self.object = [moc existingObjectWithID:self.objectID error:&error];
if (error) {
DDLogError(#"error finding object: %#", error);
}
...
}
MR_context is a MagicalRecord method which simply sets up a new context with concurrency type NSPrivateQueueConcurrencyType with its parent set to the root saving context. All of that looks correct to me.
Errors
When no debugger is set, I simply get a fault for the object. If I try to look up any property on it, I get nil.
With the debugger turned on and -com.apple.CoreData.ConcurrencyDebug 1 set I get some errors:
In the simulator I get EXC_BAD_INSTRUCTION (code=EXC_i386_INVOP, subcode=0x0) on the [moc existingObjectWithID:self.objectID error:&error] line.
On the device, I get the following on that same line:
CoreData`+[NSManagedObjectContext __Multithreading_Violation_AllThatIsLeftToUsIsHonor__]:
-> 0x185946614 <+0>: brk #0x1
So obviously something is wrong, but the objectID isn't temporary, it is the same objectID I see when inspecting the real object on the main thread. So I'm not sure why it isn't working.
I've also tried:
self.object = [moc objectWithID:self.objectID];
But that doesn't work either. Any ideas?
Updates
2015-07-10 - Ok, if I use a block with the moc then I can properly access the object from within that block.
[moc performBlock:^{
NSError *error = nil;
NSManagedObject *o = [moc existingObjectWithID:self.objectID error:&error];
}];
How do I properly extract that object so I can have access to it in the thread that the operation is running? Doing a __block property and then setting it in the block and using it out of the block failed.
Is this an issue with the way the context is setup?
MagicalRecord's context sets up its own queue to perform operations on it. Therefore, if you want to use it to look something up, you have to do it in a block ([context performBlock:] or [context performBlockAndWait:]) so that it can perform it on its own queue and then return back to the NSOperation.
What I ended up doing was creating a couple __block properties and then pulling the needed contents from the object outside of the block.

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

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;
}
}

CoreData deadlock with multiple threads

I'm experiencing the same deadlock issue (that is quite common on SO) that occurs in the multiple NSManagedObjectContexts & multiple threads scenario. In some of my view controllers, my app uses background threads to get data from a web service, and in that same thread it saves it. In others, where it makes sense to not progress any further without saving (e.g. persist values from a form when they hit "Next"), the save is done on the main thread. AFAIK there should be nothing wrong with this in theory, but occasionally I can make the deadlock happen on a call to
if (![moc save:&error])
...and this seems to be always on the background thread's save when the deadlock occurs. It doesn't happen on every call; in fact it's quite the opposite, I have to use my app for a couple of minutes and then it'll happen.
I've read all the posts I could find as well as the Apple docs etc, and I'm sure I'm following the recommendations. To be specific, my understanding of working with multiple MOCs/threads boils down to:
Each thread must have its own MOC.
A thread's MOC must be created on that thread (not passed from one thread to another).
A NSManagedObject cannot be passed, but a NSManagedObjectID can, and you use the ID to inflate a NSManagedObject using a different MOC.
Changes from one MOC must be merged to another if they are both using the same PersistentStoreCoordinator.
A while back I came across some code for a MOC helper class on this SO thread and found that it was easily understandable and quite convenient to use, so all my MOC interaction is now thru that. Here is my ManagedObjectContextHelper class in its entirety:
#import "ManagedObjectContextHelper.h"
#implementation ManagedObjectContextHelper
+(void)initialize {
[[NSNotificationCenter defaultCenter] addObserver:[self class]
selector:#selector(threadExit:)
name:NSThreadWillExitNotification
object:nil];
}
+(void)threadExit:(NSNotification *)aNotification {
TDAppDelegate *delegate = (TDAppDelegate *)[[UIApplication sharedApplication] delegate];
NSString *threadKey = [NSString stringWithFormat:#"%p", [NSThread currentThread]];
NSMutableDictionary *managedObjectContexts = delegate.managedObjectContexts;
[managedObjectContexts removeObjectForKey:threadKey];
}
+(NSManagedObjectContext *)managedObjectContext {
TDAppDelegate *delegate = (TDAppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *moc = delegate.managedObjectContext;
NSThread *thread = [NSThread currentThread];
if ([thread isMainThread]) {
[moc setMergePolicy:NSErrorMergePolicy];
return moc;
}
// a key to cache the context for the given thread
NSString *threadKey = [NSString stringWithFormat:#"%p", thread];
// delegate.managedObjectContexts is a mutable dictionary in the app delegate
NSMutableDictionary *managedObjectContexts = delegate.managedObjectContexts;
if ( [managedObjectContexts objectForKey:threadKey] == nil ) {
// create a context for this thread
NSManagedObjectContext *threadContext = [[NSManagedObjectContext alloc] init];
[threadContext setPersistentStoreCoordinator:[moc persistentStoreCoordinator]];
[threadContext setMergePolicy:NSErrorMergePolicy];
// cache the context for this thread
NSLog(#"Adding a new thread:%#", threadKey);
[managedObjectContexts setObject:threadContext forKey:threadKey];
}
return [managedObjectContexts objectForKey:threadKey];
}
+(void)commit {
// get the moc for this thread
NSManagedObjectContext *moc = [self managedObjectContext];
NSThread *thread = [NSThread currentThread];
if ([thread isMainThread] == NO) {
// only observe notifications other than the main thread
[[NSNotificationCenter defaultCenter] addObserver:[self class] selector:#selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:moc];
}
NSError *error;
if (![moc save:&error]) {
NSLog(#"Failure is happening on %# thread",[thread isMainThread]?#"main":#"other");
NSArray* detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey];
if(detailedErrors != nil && [detailedErrors count] > 0) {
for(NSError* detailedError in detailedErrors) {
NSLog(#" DetailedError: %#", [detailedError userInfo]);
}
}
NSLog(#" %#", [error userInfo]);
}
if ([thread isMainThread] == NO) {
[[NSNotificationCenter defaultCenter] removeObserver:[self class] name:NSManagedObjectContextDidSaveNotification
object:moc];
}
}
+(void)contextDidSave:(NSNotification*)saveNotification {
TDAppDelegate *delegate = (TDAppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *moc = delegate.managedObjectContext;
[moc performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:saveNotification
waitUntilDone:NO];
}
#end
Here's a snippet of the multi-threaded bit where it seems to deadlock:
NSManagedObjectID *parentObjectID = [parent objectID];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
// GET BACKGROUND MOC
NSManagedObjectContext *backgroundContext = [ManagedObjectContextHelper managedObjectContext];
Parent *backgroundParent = (Parent*)[backgroundContext objectWithID:parentObjectID];
// HIT THE WEBSERVICE AND PUT THE RESULTS IN THE PARENT OBJECT AND ITS CHILDREN, THEN SAVE...
[ManagedObjectContextHelper commit];
dispatch_sync(dispatch_get_main_queue(), ^{
NSManagedObjectContext *mainManagedObjectContext = [ManagedObjectContextHelper managedObjectContext];
parent = (Parent*)[mainManagedObjectContext objectWithID:parentObjectID];
});
});
The conflictList in the error seems to suggest that it's something to do with the ObjectID of the parent object:
conflictList = (
"NSMergeConflict (0x856b130) for NSManagedObject (0x93a60e0) with objectID '0xb07a6c0 <x-coredata://B7371EA1-2532-4D2B-8F3A-E09B56CC04F3/Child/p4>'
with oldVersion = 21 and newVersion = 22
and old object snapshot = {\n parent = \"0xb192280 <x-coredata://B7371EA1-2532-4D2B-8F3A-E09B56CC04F3/Parent/p3>\";\n name = \"New Child\";\n returnedChildId = 337046373;\n time = 38;\n}
and new cached row = {\n parent = \"0x856b000 <x-coredata://B7371EA1-2532-4D2B-8F3A-E09B56CC04F3/Parent/p3>\";\n name = \"New Child\";\n returnedChildId = 337046373;\n time = 38;\n}"
);
I've tried putting in refreshObject calls as soon as I've gotten hold of a MOC, with the theory being that if this is a MOC we've used before (e.g. we used an MOC on the main thread before and it's likely that this is the same one that the helper class will give us), then perhaps a save in another thread means that we need to explicitly refresh. But it didn't make any difference, it still deadlocks if I keep clicking long enough.
Does anyone have any ideas?
Edit: If I have a breakpoint set for All Exceptions, then the debugger pauses automatically on the if (![moc save:&error]) line, so the play/pause button is already paused and is showing the play triangle. If I disable the breakpoint for All Exceptions, then it actually logs the conflict and continues - probably because the merge policy is currently set to NSErrorMergePolicy - so I don't think it's actually deadlocking on the threads. Here's a screehshot of the state of both threads while it's paused.
I do not recommend your approach at all. First, unless you are confined to iOS4, you should be using the MOC concurrency type, and not the old method. Even under iOS 5 (which is broken for nested contexts) the performBlock approach is much more sound.
Also, note that dispatch_get_global_queue provides a concurrent queue, which can not be used for synchronization.
The details are found here: http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/CoreData/Articles/cdConcurrency.html
Edit
You are trying to manage MOCs and threading manually. You can do it if you want, but there be dragons in your path. That's why the new way was created, to minimize the chance for errors in using Core Data across multiple threads. Anytime I see manual thread management with Core Data, I will always suggest to change as the first approach. That will get rid of most errors immediately.
I don't need to see much more than you manually mapping MOCs and threads to know that you are asking for trouble. Just re-read that documentation, and do it the right way (using performBlock).

Resources