Core Data privateQueue performBlockAndWait deadlock while accessing relationship
Asked Answered
M

2

12

This topic has been discussed at many forum, but I am still not able to fully understand how performBlockAndWait actually works. As per my understanding, context.performBlockAndWait(block: () -> Void) will perform the block in its own queue while blocking the caller thread. Documentation says that:

You group “standard” messages to send to the context within a block to pass to one of these methods.

What are the "standard" messages? It also says that:

Setter methods on queue-based managed object contexts are thread-safe. You can invoke these methods directly on any thread.

Does that mean that I can set properties of a managed object which is fetched inside performBlock* API of a context outside performBlock* APIs?

As per my understanding, calling performBlockAndWait(block: () -> Void) on context with concurrency type .MainQueueConcurrencyType will create a deadlock and block UI forever when called from main thread. But in my tests, its not creating any deadlock.

The reason why I think that it should create a deadlock is that, performBlockAndWait will first block the caller thread, and then execute the block on its own thread. Since the thread in which context has to execute its block is the same as the caller thread which is already blocked, so it will never be able to execute its block and the thread remains blocked forever.

However I am facing deadlocks in some weird scenario. I have below test code:

@IBAction func fetchAllStudentsOfDepartment(sender: AnyObject) {

    let entity = NSEntityDescription.entityForName("Department", inManagedObjectContext: privateContext)
    let request = NSFetchRequest()
    request.entity = entity
    request.relationshipKeyPathsForPrefetching = ["students"]
    var department: Department?

    privateContext.performBlockAndWait { () -> Void in
        department = try! self.privateContext.executeFetchRequest(request).first as? Department
        print(department?.name)
        guard let students = department?.students?.allObjects as? [Student] else {
            return
        }
        for student in students {
            print(student.firstName)
        }
    }
}

@IBAction func fetchDepartment(sender: AnyObject) {

    let entity = NSEntityDescription.entityForName("Department", inManagedObjectContext: privateContext)
    let request = NSFetchRequest()
    request.entity = entity

    privateContext.performBlockAndWait { () -> Void in
        let department = try! self.privateContext.executeFetchRequest(request).first as? Department
        print(department?.name)

    }

    privateContext.performBlockAndWait { () -> Void in
        let department = try! self.privateContext.executeFetchRequest(request).first as? Department
        print(department?.name)
    }
}

Note that I accidentally pasted performBlockAndWait twice in fetchDepartment method in my test code.

  • It does not create any deadlock if I have not called fetchAllStudentsOfDepartment method. But once I call fetchAllStudentsOfDepartment, any call to fetchDepartment method blocks the UI forever.
  • If I remove print(student.firstName) in fetchAllStudentsOfDepartment method, then it does not block. That means, it blocks UI only if I access a relationship's property.
  • privateContext has concurrencyType set to .PrivateQueueConcurrencyType. The above code blocks UI only when privateContext's parentContext has concurrencyType set to .MainQueueConcurrencyType.

    I have tested the same code with other .xcdatamodel as well and I am sure now that it only blocks if a relationship's property is accessed. My current .xcdatamodel looks like: data model

Pardon me if the information is extraneous, but I am just sharing all my observations after spending like 8 hours already. I can post my thread stack when UI is blocked. To summarize, I have three questions:

  1. What are the "standard" messages?
  2. Can we set properties of a managed object which is fetched inside performBlock* API of a context outside performBlock*?
  3. Why performBlockAndWait is misbehaving and causing UI block in my test code.

TEST CODE: You can download the test code from here.

Markova answered 2/1, 2016 at 7:29 Comment(0)
B
8
  1. Standard messages is old Objective-C lingo. That means you should do all of the regular method calls on a ManagedObjectContext and its child ManagedObjects in the performBlock or performBlockAndWait. The only calls that are allowed on a private context outside of the block is init and setParentContext. Anything else should be done in a block.

  2. No. Any managed object fetched from a private context must only be accessed on that private context's queue. Accessing (read or write) from another queue is violating the thread confinement rules.

  3. The reason you are having blocking issues is because you have two levels of "mainQueue" contexts and that is "outsmarting" the queue system. This is the flow:

    • You create a context on the main queue and then create it as a child of another main queue context.
    • You create a private child of that second tier main queue context
    • You access that private queue context in such a way that it is trying to fault in the objects that currently are already loaded on the main queue context.

Because of the two levels of main queue contexts it is causing a deadlock where normally the queue system would see the potential deadlock and avoid it.

You can test this by changing your mainContext variable to:

lazy var mainContext: NSManagedObjectContext = {
    let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate
    return appDelegate!.managedObjectContext
}

And your issue goes away because the queue system will see the block and avoid it. You can even see that happening by putting a break point inside of the performBlockAndWait() and see that you are still on the main queue.

In the end, there is no reason to have two levels of main queue contexts like that in a parent/child design. If anything, this is a good argument NOT to do that.

Update

I missed that you had altered the template code in the appDelegate and turned the overall context into a private one.

That pattern of having a main MOC per vc throws away a lot of the benefits of Core Data. While having a private at the top and a main MOC (that exists for the entire app, not just one VC) is a valid design it won't work if you are doing performBlockAndWait like this from the main queue.

I would not recommend ever using performBlockAndWait from the main queue as you are blocking the entire application. performBlockAndWait should only ever be used when calling TO the main queue (or perhaps one background to another background).

Bassoon answered 2/1, 2016 at 21:8 Comment(5)
As an aside, calling performBlockAndWait from the main queue is almost always a bad idea.Bassoon
Thank you for the clarification. It makes sense. However I dint get "two levels of main queue contexts". There is only one context which has MainQueue concurrency. AppDelegate's context has concurrency PrivateQueue. I thought this is the recommended nested context pattern. PersistenceStore -> PrivateQueueContext -> MainQueueContext -> other PrivateQueueContext/s. If I make mainContext = appDelegate's context, then there will be no context in my app with MainQueue concurrency type.Markova
I followed pattern described in cocoanetics.com/2012/07/multi-context-coredata under 'Asynchronous Saving' section.Markova
@MarcusS.Zarra can you have a look at this related topic question ?Soothsayer
@NeilGaliaskarov looks like you are being helped already, you are in good hands with Mundi.Bassoon
P
5
  1. What are the "standard" messages?

Any message sent to the managed object context, or any managed object. Note that the documentation continues to clarify...

There are two exceptions:

* Setter methods on queue-based managed object contexts are thread-safe.
  You can invoke these methods directly on any thread.

* If your code is executing on the main thread, you can invoke methods on the
  main queue style contexts directly instead of using the block based API.

Thus, anything but a setter method on a MOC must be called from inside a performBlock. Any method on a MOC that is of NSMainQueueConcurrencyType may be called from the main thread without being wrapped inside a performBlock.

  1. Can we set properties of a managed object which is fetched inside performBlock* API of a context outside performBlock*?

No. Any access of a managed object must be protected from inside performBlock on the managed object context in which the managed object resides. Note the exception for managed objects residing in a main-queue MOC being accessed from the main queue.

  1. Why performBlockAndWait is misbehaving and causing UI block in my test code.

It is not misbehaving. performBlockAndWait is reentrant, but only when already processing a performBlock[AndWait] call.

You should never use performBlockAndWait unless you have no other option. It is especially problematic with nested contexts.

Use performBlock instead.

Perry answered 2/1, 2016 at 22:18 Comment(2)
Should not write posts while watching football (Arkansas/KansasState)... got distracted, and by the time I posted at halftime, Marcus had already answered... but I didn't see it until after I posted. Sorry for the redundant post.Perry
Thank you. Makes sense. I should not use performBlockAndWait. But if I have to use, I should not at least call it from the main thread.Markova

© 2022 - 2024 — McMap. All rights reserved.