Core Data concurrency `performBlockAndWait:` NSManagedObjectContext zombie
Asked Answered
M

2

0

I have a following crash report from my released app:

enter image description here

synchronizeMyWords method fetches the entities from database, creates private queue context with main context parent and finally saves results. All operations are in the background thread. This method being called every time app goes into background and foreground. Here is a simplified method:

- (AWSTask *)synchronizeMyWords {
  __weak typeof(self) weakSelf = self;

  AWSContinuationBlock block = ^id _Nullable(AWSTask * _Nonnull task) {
    if ([task.result isKindOfClass:[NSArray class]]) {
      NSArray * records = (NSArray *)task.result;
      NSManagedObjectContext * context = [NSManagedObjectContext MR_contextWithParent:[NSManagedObjectContext MR_defaultContext]];
      [context performBlockAndWait:^{
        for (NSDictionary * info in records) {
            [RDRWord MR_createEntityInContext:context];
        }

        [context save:nil];
      }];
      return [AWSTask taskWithResult:@YES];
    }
    return [AWSTask taskWithError:[NSError errorWithDomain:@"" code:404 userInfo:nil]];
  };

  AWSExecutor * executor = [AWSExecutor defaultExecutor];


  return [[self loadLocalWords] continueWithExecutor:executor withBlock:block];
}

As you see I am using Magical Record 3rd party library to manage Core Data stack. Here is a method of creating private queue context:

+ (NSManagedObjectContext *) MR_contextWithParent:(NSManagedObjectContext *)parentContext
{
    NSManagedObjectContext *context = [self MR_newPrivateQueueContext];
    [context setParentContext:parentContext];
    [context MR_obtainPermanentIDsBeforeSaving];
    return context;
}

You can check the whole NSManagedObjectContext+MagicalRecord category on github here.

How is it available that context object inside performBlockAndWait: released before it escapes the scope? I am personally not able to reproduce the crash, but a lot of my users (iOS 8.1 - 10 devices) are affected by this issue.

UPDATE 1:

Here is for instance same report on blog

Moccasin answered 10/12, 2016 at 7:39 Comment(0)
K
1

Core Data provides ample APIs to deal with background threads. These are also accessible via Magical Record.

It looks as if you creating too many threads unnecessarily. I think that the employment of AWSContinuationBlock and AWSExecutor is not a good idea. synchronizeMyWords could be called from a background thread. The block might be run on a background thread. Inside the block you create a new background thread linked to the child context. It is not clear what loadLocalWords returns, or how continueWithExecutor:block: deals with threads.

There is also a problem with the saving of the data. The main context is not saved after the child context is saved; presumably this happens later, but perhaps in connection with some other operation, so that the fact that your code was working before is perhaps more of a "false positive".

My recommendation is to simplify the threading code. You should confine yourself to the Core Data block APIs.

Kotto answered 10/12, 2016 at 9:31 Comment(1)
Correct main context is not saved; main context is being saved when app goes background. It is separate operation. continueWithExecutor:block: means that the continuationBlock runs in background thread. Task here is BFTask subclass, via link brief description.Moccasin
M
2

I marked @Mundi answer as correct, because he wrote the general approach you should follow. Now, I want to share here how I debugged it. Firstly, I learned that, it is available to turn on debug concurrency assertion in xcode. You need to pass following argument on launch:

-com.apple.CoreData.ConcurrencyDebug 1

enter image description here

Now, in your application output, you should see log message:

2016-12-12 01:58:31.665 your-app[4267:2180376] CoreData: annotation: Core Data multi-threading assertions enabled.

Once I turned it on, my app crashed in synchronizeMyWords method (honestly, not only there. Wondering, why Apple does not include concurrency assertions by default in debug mode?). I checked what defaultExecutor is in AWSCore library and saw this:

+ (instancetype)defaultExecutor {
    static AWSExecutor *defaultExecutor = NULL;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        defaultExecutor = [self executorWithBlock:^void(void(^block)()) {
            // We prefer to run everything possible immediately, so that there is callstack information
            // when debugging. However, we don't want the stack to get too deep, so if the remaining stack space
            // is less than 10% of the total space, we dispatch to another GCD queue.
            size_t totalStackSize = 0;
            size_t remainingStackSize = remaining_stack_size(&totalStackSize);

            if (remainingStackSize < (totalStackSize / 10)) {
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block);
            } else {
                @autoreleasepool {
                    block();
                }
            }
        }];
    });
    return defaultExecutor;
}

According to their if statement, my continuationBlock was not guaranteed to be executed on DISPATCH_QUEUE_PRIORITY_DEFAULT queue. So, I created one shared dispatch_queue_t queue and call all operations on it combining with performBlockAndWait: CoreData method. As a result, there are no crashes now and I submitted new release. I will update this post, if I do not get any crash report with context zombie.

Moccasin answered 12/12, 2016 at 6:2 Comment(1)
Thanks for sharing! It think this is useful for others.Kotto
K
1

Core Data provides ample APIs to deal with background threads. These are also accessible via Magical Record.

It looks as if you creating too many threads unnecessarily. I think that the employment of AWSContinuationBlock and AWSExecutor is not a good idea. synchronizeMyWords could be called from a background thread. The block might be run on a background thread. Inside the block you create a new background thread linked to the child context. It is not clear what loadLocalWords returns, or how continueWithExecutor:block: deals with threads.

There is also a problem with the saving of the data. The main context is not saved after the child context is saved; presumably this happens later, but perhaps in connection with some other operation, so that the fact that your code was working before is perhaps more of a "false positive".

My recommendation is to simplify the threading code. You should confine yourself to the Core Data block APIs.

Kotto answered 10/12, 2016 at 9:31 Comment(1)
Correct main context is not saved; main context is being saved when app goes background. It is separate operation. continueWithExecutor:block: means that the continuationBlock runs in background thread. Task here is BFTask subclass, via link brief description.Moccasin

© 2022 - 2024 — McMap. All rights reserved.