Xcode 7.0 IOS9 SDK: deadlock while executing fetch request using executeBlockAndWait

Updated: I prepared a sample that reproduces the problem without magic recording. Please download the test project using the following URL: https://www.dsr-company.com/fm.php?Download=1&FileToDL=DeadLockTest_CoreDataWithoutMR.zip

The provided project has the following problem: deadlock on extraction in executeBlockAndWait, called from the main thread.

The problem is reproduced if the code is compiled using Xcode version> 6.4. The problem does not reproduce if the code is compiled using xCode == 6.4.

Old question:

I am working on support for IOS mobile application. After the recent upgrade of the Xcode IDE from version 6.4 to version 7.0 (with support for iOS 9), I had a critical problem - application freezing. The same build of the application (from the same sources) with xCode 6.4 works fine. So, if the application is built using xCode> 6.4, the application freezes in some cases. if the application is built using xCode 6.4 - the application works fine.

I spent some time researching the problem, and as a result I prepared a test application with a similar case, as in my application, which reproduces the problem. Capture test application on Xcode> = 7.0, but works correctly on Xcode 6.4

Download link to test sources: https://www.sendspace.com/file/r07cln

Requirements for the test application: 1. cocoa pods manager must be installed on the system 2. Magic area of ​​the Registry version 2.2.

The application for testing works as follows: 1. At the beginning of the application, it creates a test database with 10,000 records of simple objects and stores them in a permanent storage. 2. On the first screen of the application, in the viewWillAppear method: it runs a test that causes a dead end. The following algorithm is used:

-(NSArray *) entityWithId: (int) entityId inContext:(NSManagedObjectContext *)localContext { NSArray * results = [TestEntity MR_findByAttribute:@"id" withValue:[ NSNumber numberWithInt: entityId ] inContext:localContext]; return results; } ….. int entityId = 88; NSManagedObjectContext *childContext1 = [NSManagedObjectContext MR_context]; childContext1.name = @"childContext1"; NSManagedObjectContext *childContext2 = [NSManagedObjectContext MR_context]; childContext2.name = @"childContext2"; NSArray *results = [self entityWithId:entityId inContext: childContext2]; for(TestEntity *d in results) { NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); /// this line is the reason of the hangup } 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); } }); 

Two managed entity contexts are created using concurrency type == NSPrivateQueueConcurrencyType (please check the MR_context code from the magic record structure). Both contexts have a parent context with concurrency type = NSMainQueueConcurrencyType. In the main thread application, synchronization fetch is performed (MR_findByAttribute and MR_findAllWithPredicate use executeBlockAndWait with a fetch request inside). After the first sample, the second sample is the schedule for the main thread using dispatch_async ().

As a result, the application freezes. There seems to be a deadlock, check out the screenshot of the stack:

 here is the link, my reputation is too low to send images. https://cdn.img42.com/34a8869bd8a5587222f9903e50b762f9.png )

If you comment a line
NSLog (@ "e from fetchRequest% @ with name = '% @'", d, d.name); /// this line is the cause of the hang

(this is line 39 in the ViewController.m test project), the application becomes operational. I believe this is because there is no field for the name of the test object.

So, with the commented line NSLog (@ "e from fetchRequest% @ with the name = '% @'", d, d.name); there is no freeze on binaries built with both Xcode 6.4 and Xcode 7.0.

With uncommented NSLog line (@ "e from fetchRequest% @ with name = '% @'", d, d.name);

the binary built with Xcode 7.0 has a hang, and the binary built with Xcode 6.4 has no hang.

I believe the problem is due to lazy loading of entity data.

Are there any problems with the described case? I will be grateful for any help.

+5
source share
2 answers

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); /// this line is the reason of the hangup } 

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); } }]; 
+6
source

We switched to XCode7 and I just ran into a similar deadlock problem with performBlockAndWait in code that works fine in XCode6.

The problem is using dispatch_async(mainQueue, ^{ ... for the upstream to pass the result from the network operation. This call was no longer needed after adding concurrency support for CoreData, but for some reason it stayed and never caused a problem so far .

Apple may have changed something behind the scenes to make more dead ends more explicit.

+1
source

Source: https://habr.com/ru/post/1232711/


All Articles