Core Data: parent context blocks child

2019-02-10 16:38发布

I'm doing some background processing in an app with core data. The background processing is done on a child managedObjectContext. Context initialization:

appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate

// the moc in appDelegate is created with .MainQueueConcurrencyType
mainThreadMOC = appDelegate.managedObjectContext!
backgroundMOC = NSManagedObjectContext(concurrencyType:NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType)
backgroundMOC?.parentContext = mainThreadMOC

Background processing is done in the following method:

// download all new transaction log entries
func syncItems() {

... set up the query object for parse

let moc = CoreDataStore.sharedInstance.backgroundMOC

// perform download
moc?.performBlock( {
    self.runQuery(query)   // Download stuff und do some core data work
    })
}

The debugger shows that all work inside the block is indeed in a background thread.

When I call this function from the main thread and immediately block the main thread (for test purpose) with a lengthy core data operation, I see that the background thread stops and only continues execution when the main thread is idle.

// this is called from a view controller in the main thread

syncItems() // should start to work in background
for i in 0...200 {
    // do some core data work in main thread
}
// syncItems starts to work after the blocking for-loop ends.

Why is that happening?

4条回答
对你真心纯属浪费
2楼-- · 2019-02-10 17:20

Regarding parent contexts, unfortunately the last time I checked the documentation was fairly spartan, and online you will find that nearly every single person has inverted the flow. There was one WWDC session on Core Data back in 2011 or so where everything was made clear, and if you look at the API carefully, it will start to make sense.

The child is NOT the background context - it is the main context. The child is the context you are generally interacting with. The parent is the background context.

The child pushes changes to its parent. That is why the parent has a persistentStoreCoordinator, but the child instead has a parentContext (it does NOT need a persistentStoreCoordinator).

So the child is your main context, on the main (UI) queue. It saves changes up into its parent. This happens in memory (fast - leaving the UI as responsive as possible).

The parent context then saves its changes into the persist store via its persistentStoreCoordinator. That is why the parent is on a private queue.

lazy var managedObjectContext: NSManagedObjectContext = {
    let parentContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
    parentContext.persistentStoreCoordinator = self.persistentStoreCoordinator

    let managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
    managedObjectContext.parentContext = parentContext
    return managedObjectContext
}()

There are other optimizations, such as setting the .undoManager to nil, but this general architecture works flawlessly for backgrounding saves.

You probably also want to put in a save method that receives a completion block/closure, immediately save on your child queue (which only saves into the parent, as mentioned), then call the parentContext's performBlock method in which you will have it save (on its private queue) into the underlying persistent store (comparatively slow, but now non-blocking), and then call your completion block/closure (which you either already set up using GCD to run back on the main queue, or else you call back to the main queue from there in the parent's performBlock method.

Everything falls into place perfectly fine when you don't invert the architecture. I'm not sure how it got started online, but it's a nearly universal mistake.

Best of luck.

查看更多
smile是对你的礼貌
3楼-- · 2019-02-10 17:21

Don’t use a parent-child context setup.

Parent-child context are not a good approach for just about anything. Just use a simple stack: two contexts with one shared persistent store coordinator.

Parent-child contexts just add a lot of confusion without giving you much of anything. It’s a pretty misunderstood concept. I wish people like mzarra would f***ing stop advocating that setup. It’s a disservice to the community.

If your background context is a child context of your main context, you will have to lock both the main and the background context, whenever the background context needs to save. This blocks the UI. And you’ll have to lock the UI a 2nd time to propagate those changes from the UI into the PSC. If you use a background context, you will have to merge changes into the main context, but you'll only do work for those objects that are currently registered with that context. If you added new objects, or updated / deleted objects that are not currently registered (referenced) that’s basically a no-op.

查看更多
淡お忘
4楼-- · 2019-02-10 17:21

I suspect your problem is twofold, in that, even though you are doing for test purposes, your

lengthy core data operation

on the main thread is blocking the UI, and by your definition your backgroundMOC?.parentContext = mainThreadMOC.

I'd recommend creating a more robust structure for your multiple NSManagedObjectContexts.

I recommend you follow the steps outlined in this answer.

In addition, you can create an additional MOC specifically to manage your runQuery...

discreteMOC = NSManagedObjectContext(concurrencyType:NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType)
discreteMOC?.parentContext = backgroundMOC //MOCprivate in my previous answer. 

Hope this helps.

查看更多
The star\"
5楼-- · 2019-02-10 17:39

When you say in the comment "do some core data work in main thread" is that work accessing mainThreadMOC?

It sounds like perhaps the main thread work is locking something that runQuery needs to access.

Try changing the testing busy-work that blocks the main thread to something that doesn't access Core Data (NSThread.sleepForTimeInterval should do) and see if that lets the background runQuery work.

If that's the problem you'll need to refactor the main thread work into something that doesn't block whatever is happening in runQuery.

查看更多
登录 后发表回答