Question

I've seen this problem addressed before, but none of the solutions seem to have any effect in my situation, which is this:

My app uses three ManagedObjectContexts:

1) The "diskManagedObjectContext", with NSPrivateQueueConcurrencyType and no parent context, is created on the global (background) queue and used to write context changes to disk (persistent store) on a background thread:

-(NSManagedObjectContext *)diskManagedObjectContext
{
    if (! _diskManagedObjectContext)
    {
        dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
        ^{
            _diskManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
            _diskManagedObjectContext.persistentStoreCoordinator = self.ircManagedObjectLibrary.persistentStoreCoordinator;
        });
    }

    return _diskManagedObjectContext;
}

2) The "mainManagedObjectContext", with NSMainQueueConcurrencyType, has the diskManagedObjectContext as its parent context, is created on the main thread (at app launch) and is used by all GUI processes (view controllers, etc.). Saves on the mainManagedObjectContext simply push changes "up" to its parent, the diskManagedObjectContext, which will then asynchronously write the changes to disk.

-(NSManagedObjectContext *)mainManagedObjectContext
{
    if (! _mainManagedObjectContext)
    {
        _mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        _mainManagedObjectContext.undoManager = nil;
        _mainManagedObjectContext.parentContext = self.diskManagedObjectContext;
    }

    return _mainManagedObjectContext;
}

3) The "backgroundManagedObjectContext", with NSPrivateQueueConcurrencyType, has the mainManagedObjectContext as its parent context, is created on the global (background) queue and is used by all non-GUI processes (data archivers, logging, etc.) to make relatively-low-priority model changes in the background. Saves on backgroundManagedObjectContext simply push the changes "up" to its parent, the mainManagedObjectContext, at which point GUI elements listening for model changes of relevance to them get events and update accordingly.

-(NSManagedObjectContext *)backgroundManagedObjectContext
{
    if (! _backgroundManagedObjectContext)
    {
        dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
        ^{
            _backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
            _backgroundManagedObjectContext.undoManager = nil;
            _backgroundManagedObjectContext.parentContext = self.mainManagedObjectContext;
        });
    }

    return _backgroundManagedObjectContext;
}

I've implemented the nested save behavior described above (backgroundMOC —(sync)—> mainMOC —(async)—> diskMOC —(async)—> disk) thusly, as a method on what I call a "ManagedObjectLibrarian" class (of which there are two instances, one holding and encapsulating the mainMOC, and the other the backgroundMOC):

-(BOOL)saveChanges:(NSError **)error
{
    __block BOOL successful = NO;

*[DEADLOCKS HERE]*  [self.managedObjectContext performBlockAndWait:  *[SYNCHRONOUS]*
    ^{
        NSError *internalError = nil;

        // First save any changes on the managed object context of this ManagedObjectLibrarian. If the context has a parent, this does not write changes to disk but instead simply pushes the changes to the parent context in memory, and so is very fast.

        successful = [_managedObjectContext save:&internalError];

        if (successful)
        {
           // If successful, then if the context of this ManagedObjectLibrarian has a parent with private queue concurrency (which only the app delegate's mainIrcManagedObjectLibrarian does), save the changes on that parent context, which if it does not itself have a parent will write the changes to disk. Because this write is performed as a block operation on a private queue, it is executed in a non-main thread.

           if (_managedObjectContext.parentContext)
           {
              [_managedObjectContext.parentContext performBlock:  *[ASYNCHRONOUS]*
              ^{
                  if (_managedObjectContext.parentContext.concurrencyType == NSPrivateQueueConcurrencyType)
                  {
                      NSError *parentError = nil;
                      BOOL parentSuccessful = [_managedObjectContext.parentContext save:&parentError];

[Error handling, etc.]

The first of these nested saves is performed in a performBlockAndWait for several reasons:

1) It follows the pattern of the MOC's own save method, which returns a BOOL success result (and so must not return until after the initial save is finished or fails). 2) Since both the mainMOC and the backgroundMOC have parent contexts, their saves only push changes up to their parents, and so are very fast (and thus don't need to be performed asynchronously, unlike the diskMOC save).

(In case it matters: the reason I perform background ManagedObjectLibrarian and main ManagedObjectLibrarian saves in a performBlock(AndWait): on their respective MOCs in the first place is so that I can execute those saves from any thread as needed.)

All of this is, I think, pretty typical, and so hopefully, so far so good.

[End of Preamble]

Here's the problem: I am encountering apparent deadlocks in or at the start of the

 [self.managedObjectContext performBlockAndWait:  *[SYNCHRONOUS]*

at the top of this saveChanges method. This is happening even though the call to this method is not itself being performed within a performBlockAndWait: block — in fact, the first deadlock I encounter is on app launch, when the following is called on the main thread:

[self.backgroundManagedObjectLibrarian saveChanges:nil];

Looking at the thread states when this deadlock occurs, the other blocking thread is deadlocked at the call to a MOC's save: method in a performBlock on it:

[self.dataSourceProperties.managedObjectContext performBlock: *[ASYNCHRONOUS]*
^{
    self.dataSourceProperties.dataArchiving.isEnabled = [NSNumber numberWithBool:_dataArchivingIsEnabled];

*[DEADLOCKS HERE]* [self.dataSourceProperties.managedObjectContext save:nil];
}];

The dataArchiving.isEnabled model entity attribute change that is being saved here was made in a peformBlockAndWait: on the same backgroundManagedObjectLibrarian; but the call above to saveChanges: on the backgroundManagedObjectLibrarian that deadlocks is made outside and just after that block return.! So I don't see how the earlier, asynchronous (peformBlock:) save within a synchronous (peformBlockAndWait:) block, can possibly be blocking this subsequent synchronous (peformBlockAndWait:) save outside the synchronous block.

What's more, if I change this model-entity-attribute-change save operation from a performBlock: to performBlockAndWait:, no deadlock occurs (well, not until the execution reaches the next asynchronous model-entity-attribute-change change save).

I suppose I could, of course, go through and make all my asynchronous model-entity-attribute-change saves synchronous, but I feel like I shouldn't have to do that, especially since I don't need those changes to propagate immediately, nor to stick around for any returns or results.

[Questions]

SO, questions:

1) Just why am I getting a deadlock here? 2) What am I doing wrong and/or misunderstanding? 3) And just what is the right pattern for managing both nested MOC saves and individual model-entity-attribute-change saves?

Thanks!

Carl

Was it helpful?

Solution

Your implementation has a number of issues.

  1. Method diskManagedObjectContext and backgroundManagedObjectContext

    Use a global concurrent queue, which makes no sense. If you want to make methods thread safe (considering the instance variables are shared resources), you need to use a dedicated queue (either serial or concurrent) and use dispatch_barrier_sync to return a value and dispatch_barrier_async to write a value.

  2. Your saveChanges method SHOULD be asynchronous

    A simple and generic implementation may look as follows:

    - (void) saveContextChainWithContext:(NSManagedObjectContext*)context 
                              completion:(void (^)(NSError*error))completion
    {
        [context performBlock:^{
            NSError* error;
            if ([context save:&error]) {
                NSManagedObjectContext* ctx = [context parentContext];
                if (ctx) {
                    dispatch_async(dispatch_get_global(0,0), ^{
                        [self saveContextChainWithContext:ctx completion:completion];
                    });
                }
                else {
                    if (completion) {
                        completion(nil);
                    }
                }
            }
            else {
                if (completion) {
                    completion(error);
                }
            }
        }];
    }
    

Generally, using a synchronous blocking version of a method is always prone to dead locks - even though Core Data strives to avoid such scenarios in a best effort manner.

Thus, think asynchronous wherever possible - and you don't suffer dead locks. Here in your scenario, it's easy to employ a non-blocking asynchronous style.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top