Is using another CoreData context inside perform method of a context a problem?

Hi,

in your book “Concurrency by Tutorials” in “Chapter 11: Core Data” you are writing on page 98: “Core Data will generate a new context on a private queue for you to work with so that you don’t have to worry about any concurrency issues. Be sure not to use any other context in the closure or you’ll run into tough to debug runtime issues.

In this tutorial https://www.raywenderlich.com/7586-multiple-managed-object-contexts-with-core-data-tutorial which my code implementation is based on, you’re saving the main context (comment //3) inside the perform block of a child context:

// 2
  context.perform {
    do {
      try context.save()
    } catch let error as NSError {
      fatalError("Error: \(error.localizedDescription)")
    }
    // 3
    self.coreDataStack.saveContext()
  }

Is this against what is written in the book or am I misunderstanding the sentence above?

I’m asking because my code based on the tutorial leads to runtime crashes that are tough to debug.

For example:

OS Version: iOS 12.3.1 (16F203)
Report Version: 104

Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Codes: BUS_NOOP at 0x0000000400650068
Crashed Thread: 15

Application Specific Information:
KB372ZCVNWQPR6 >
Attempted to dereference garbage pointer 0x400650068.

Thread 15 Crashed:
0   libobjc.A.dylib                 0x3057d8ca8         objc_release
1   libobjc.A.dylib                 0x3057dab98         [inlined] (anonymous namespace)::AutoreleasePoolPage::pop
2   libobjc.A.dylib                 0x3057dab98         (anonymous namespace)::AutoreleasePoolPage::pop
3   CoreData                        0x30bf10bcc         developerSubmittedBlockToNSManagedObjectContextPerform
4   libdispatch.dylib               0x305fb67d0         _dispatch_client_callout
5   libdispatch.dylib               0x305f91de8         _dispatch_lane_serial_drain$VARIANT$armv81
6   libdispatch.dylib               0x305f92928         _dispatch_lane_invoke$VARIANT$armv81
7   libdispatch.dylib               0x305f9ae04         _dispatch_workloop_worker_thread
8   libsystem_pthread.dylib         0x3062e0110         _pthread_wqthread

I’m not quite sure what the core data stack is that they’re using, but based on what you’ve shown, it doesn’t seem to make sense to use. You’ve already go the context and you’ve saved it directly. Is this a nested/child context you’re using, or did you just do something like context = this.coreDataStack.context?

If it’s the latter, then don’t call into the coreDataStack again as

  1. It’s a redundant lookup
  2. You’re creating a retain cycle.

You’ve accessed self in your perform block, and if your controller goes away before that perform block finishes, you can have issues. Based on the error you show, I’m guessing that’s what happened…self no longer exists, and you’re then trying to access it.

@mmorey Can you chime in here? I see you wrote the tutorial being referenced.

Hi gargoyle, thanks for the quick reply! The code snippet I posted was directly taken from the tutorial, it’s not my own code: In the final project it’s in JournalListViewController method didFinish from Line 300. The variable context on which perform is called is a child context and self.coreDataStack.saveContext() saves the main context.
But now I noticed a little difference between my own and the tutorials implementation: my child context uses .privateQueueConcurrencyType whereas in the tutorial both contexts use .mainQueueConcurrencyType. Maybe this is my problem?

You’re right, that’s why it looks a little different in my implementation:

    // save child context with .privateQueueConcurrencyType
    saveContext(context) { [weak self] (success) in
        guard let strongSelf = self else { return }
        
        if success {
            // Lets save the parent if existing
            guard let parentContext = context.parent else {
                completion?(true)
                return
            }
            // save main context with .mainQueueConcurrencyType
            strongSelf.saveContext(parentContext, completion: completion)
        } else {
            completion?(false)
        }
    }

This whole code block is inside the context.perform block of the child context (var context)

Is strongSelf.saveContext using a perform type call before it does the save to put itself into the proper context?

No, the code I posted in my previous comment is wrapped inside a perform block of the child context. That’s the only perform call in this code. It’s a lot more code separated into different methods, but if I break it down it’s basically this:

let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = mainContext
context.perform { [weak self]
    guard let strongSelf = self else { return }
    // save child context with .privateQueueConcurrencyType
    strongSelf.saveContext(context) { [weak self] (success) in
        guard let strongSelf = self else { return }
        
        if success {
            // Lets save the parent if existing
            guard let parentContext = context.parent else {
                completion?(true)
                return
            }
            // save main context with .mainQueueConcurrencyType
            strongSelf.saveContext(parentContext, completion: completion)
        } else {
            completion?(false)
        }
    }
}

You’re gonna need to post this in the Core Data are I think as it’s really not a concurrency issue, and they’ll probably need to see your saveContext method. However, I can say, if saveContext isn’t doing a perform against the context you’re trying to save you’ll fail. All core data calls have to be performed in the right Core Data context.

As I took a look into the saveContext method it seems not to do it the right way:

fileprivate func saveContext(_ context: NSManagedObjectContext, completion: SuccessCompletion?) {
    
    if context.concurrencyType == .mainQueueConcurrencyType && Thread.isMainThread == false {
        DispatchQueue.main.async(execute: {
            do {
                try context.save()
                completion?(true)
            } catch {
                completion?(false)
            }
        })
    } else {
        do {
            try context.save()
            completion?(true)
        } catch {
            completion?(false)
        }
    }
}

If I understand correctly it should look like this:

fileprivate func saveContext(_ context: NSManagedObjectContext, completion: SuccessCompletion?) {
    
    context.perform {
        do {
            try context.save()
            completion?(true)
        } catch {
            completion?(false)
        }
    }
}

But from the top view this would mean calling a mainContext.perform block inside a childContext.perform block. Is this the recommended way of saving both child and main context? Or should I it look like this maybe:

childContext.perform {
	// Do some stuff like insert entities
    childContext.save()
}
mainContext.perform {
    mainContext.save()
}

Several comments:

  1. For an child context that will be edited, then saved to the parent, it makes more sense to have both use the same concurrency type. The separate private type for a child is usually used for a long running fetch or process.

  2. Perform is an async call. If you tried what you show in the last block, the second perform would get started right after the first one, which would not give the expected result. If you nest perform calls, as you did earlier, it will work as long as the two types are different, but could deadlock if both contexts were the same concurrency type.

  3. You could try PerformAndWait, but that defeats the purpose of using a separate concurrency type, you might as well just do both on the main queue.

  4. If the main context is parented by the store coordinator, it is common to only save it when the app goes to background or is exiting. Loss of data doing that is very rare. So you could just save the child context, and carry on. (That is less desirable if you are also synching with iCloud, though).

Thanks a lot! That answered all my questions I had about this issue so far. I think we gonna use a solution based on 4. As we don’t have any data sync with iCloud until now.