This is why I do not use frameworks that abstract (i.e. hide) too many details of the main data. It has very complex usage patterns, and sometimes you need to know the details of how they interact.
First of all, I don’t know anything about magic recording, except that many people use it, so it should be very good at what it does.
However, I immediately saw a few examples of the misuse of the concurrency master data in your examples, so I went and looked at the header files to see why your code made assumptions about what it was doing.
I don’t want bash at all, although this may seem red at first glance. I want to help educate you (and I used this as an opportunity to take a look at MR).
With a very quick look at MR, I would say that you have some misunderstandings about what MR does, as well as the basic general concurrency rules.
First you say it ...
Two managed entity contexts are created using concurrency type == NSPrivateQueueConcurrencyType (check the MR_context code for the magic basis of the entry). Both contexts have a parent context with concurrency type = NSMainQueueConcurrencyType.
which does not seem true. Two new contexts are, of course, private queue contexts, but their parent (according to the code I looked at on github) is the magic MR_rootSavingContext , which itself is also a private queue context.
Let me break your sample code.
NSManagedObjectContext *childContext1 = [NSManagedObjectContext MR_context]; childContext1.name = @"childContext1"; NSManagedObjectContext *childContext2 = [NSManagedObjectContext MR_context]; childContext2.name = @"childContext2";
So now you have two private-private MOCs ( childContext1 and childContext2 ), both children from another anonymous private MOC (we will call savingContext ).
NSArray *results = [self entityWithId:entityId inContext: childContext2];
Then you fetch on childContext1 . This code is actually ...
-(NSArray *) entityWithId:(int)entityId inContext:(NSManagedObjectContext *)localContext { NSArray * results = [TestEntity MR_findByAttribute:@"id" withValue:[NSNumber numberWithInt:entityId] inContext:localContext]; return results; }
Now we know that localContext in this method is in this case another pointer to childContext2 , which is the MOC of the private queue. This is 100% against concurrency rules for accessing MOC from a private queue outside of a performBlock call. However, since you are using a different API, and the method name does not help to find out how it is accessed, we need to look at this API and see if it performBlock , whether you are accessing it correctly.
Unfortunately, the documentation in the header file does not provide any guidance, so we should consider the implementation. This call ends with a call to MR_executeFetchRequest... , which does not indicate in the documentation how it handles concurrency. So, we will consider its implementation.
Now we get somewhere. This function tries to access the MOC performBlockAndWait , but uses performBlockAndWait , which will block when it is called.
This is an extremely important piece of information because calling it from the wrong place can really cause a dead end. Thus, you should be aware that performBlockAndWait is called at any time when you execute a fetch request. My personal rule is to never use performBlockAndWait unless there is absolutely no other option.
However, this call should be completely safe here ... assuming that it is not called from the context of the parent MOC.
So let's look at the following code snippet.
for(TestEntity *d in results) { NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name);
Now this is not a MagicalRecord error, because MR is not even used directly here. However, you were trained to use those MR_ methods that do not require knowledge of the concurrency model, so you either forget or never learn the concurrency rules.
The objects in the results array are all managed objects that live in the context of childContext2 private-queue. That way, you can never access them without paying tribute to concurrency rules. This is a clear violation of concurrency rules. When developing your application, you must enable concurrency debugging with the -com.apple.CoreData.ConcurrencyDebug 1 argument.
This piece of code should be wrapped in either performBlockAndWait or performBlockAndWait . I almost never used performBlockAndWait for anything because it has so many flaws that one of them is deadlocks. In fact, just the use of performBlockAndWait is a very strong sign that your deadlock is happening there, and not in the line of code that you specify. However, in this case, it is at least safe, as the previous choice, so let it become more secure ...
[childContext2 performBlockAndWait:^{ for (TestEntity *d in results) { NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); } }];
Then you send the main thread. Is it because you just want something to happen in the next cycle of the event loop, or is it because this code is already running on some other thread? Who knows. However, you have the same problem (I reformatted your code for readability as a message).
dispatch_async(dispatch_get_main_queue(), ^{ int entityId2 = 11; NSPredicate *predicate2 = [NSPredicate predicateWithFormat:@"id=%d", entityId2]; NSArray *a = [TestEntity MR_findAllWithPredicate:predicate2 inContext:childContext2]; for (TestEntity *d in a) { NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); } });
Now we know that the code runs in the main thread, and the search will call performBlockAndWait , but your subsequent access to the for-loop again violates the rules of the main concurrency data.
Based on this, the only real problems that I see are ...
MR seems to abide by the basic concurrency data rules in its API, but you must follow the concurrency master data rules when accessing managed objects.
I really don't like the use of performBlockAndWait , as this is just a problem waiting for this to happen.
Now let's look at a screenshot of your freeze. Hmm ... this is a classic deadlock, but it’s pointless because the deadlock occurs between the main thread and the MOC thread. This can only happen if the MOC of the main queue is the parent of this MOC of the private queue, but the code indicates that it is not.
Hmmm ... that didn't make sense, so I downloaded your project and looked at the source code in the block you downloaded. Now this version of the code uses MR_defaultContext as the parent of all MR_context created using MR_context . Thus, the default MOC, indeed, is the MOC of the main queue, and now it all makes sense.
You have a MOC as a child of the main queue MOC. When you send this block to the main queue, it now works as a block in the main queue. The code then calls performBlockAndWait in a context that is a child of the MOC for this queue, which is a huge no-no, and you are almost guaranteed to get into a dead end.
So, it seems that MR has since changed its code to use the main queue as the parent of new contexts to use the private queue as the parent of new contexts (most likely due to this exact problem). So, if you are upgrading to the latest MP, you should be fine.
However, I still warn you that if you want to use MR in multi-threaded methods, you must know exactly how they handle concurrency rules, and you must also be sure to listen to them at any time when you call anyone to the kernel, data objects that do not pass through the MR API.
Finally, I just say that I made tons and tons of master data, and I never used an API that tries to hide concurrency problems from me. The reason is that there are too many small corner cases, and I would rather just deal with them in a pragmatic way.
Finally, you'll almost never use performBlockAndWait unless you know exactly why its only option. If it will be used as part of the API below you, you’re even worse ... at least for me.
I hope this little walk enlightened and helped you (and possibly some others). This certainly shed some light for me and helped restore some of my previous unreasonable fears.
Edit
This is a response to the non-magic recording example you presented.
The problem with this code is the same as I described above regarding what was happening with MR.
You have a private queue context as a child in the context of the main queue.
You run the code in the main queue, and you call performBlockAndWait in the child context, which should then lock its parent context when it tries to fetch.
It's called a dead end, but the more descriptive (and seductive) term is a deadly hug.
The source code runs in the main thread. He invokes a child’s context to do something, and he doesn’t do anything until this child is completed.
For this child to complete, you need the main thread to do something. However, the main thread cannot do anything until the child is executed ... but the child is waiting for the main thread to do something ...
None of them can move forward.
The problem you are facing is very well documented and is actually mentioned several times in WWDC reports and several documents.
You should NEVER call performBlockAndWait in a child context.
The fact that you left with him in the past is simply an “accident,” because he should not work that way at all.
In fact, you hardly need every call to performBlockAndWait .
You really have to get used to asynchronous programming. Here's how I would recommend that you rewrite this test, and be that as it may, this caused this problem.
First rewrite the sample so that it works asynchronously ...
- (void)executeFetchRequest:(NSFetchRequest *)request inContext:(NSManagedObjectContext *)context completion:(void(^)(NSArray *results, NSError *error))completion { [context performBlock:^{ NSError *error = nil; NSArray *results = [context executeFetchRequest:request error:&error]; if (completion) { completion(results, error); } }]; }
Then you change the code that calls the fetch to do something like this ...
NSFetchRequest *request = [[NSFetchRequest alloc] init]; [request setEntity: testEntityDescription ]; [request setPredicate: predicate2 ]; [self executeFetchRequest:request inContext:childContext2 completion:^(NSArray *results, NSError *error) { if (results) { for (TestEntity *d in results) { NSLog(@"++++++++++ e from fetchRequest %@ with name = '%@'", d, d.name); } } else { NSLog(@"Handle this error: %@", error); } }];