问题
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 callfetchAllStudentsOfDepartment
, any call tofetchDepartment
method blocks the UI forever. - If I remove
print(student.firstName)
infetchAllStudentsOfDepartment
method, then it does not block. That means, it blocks UI only if I access a relationship's property. privateContext
hasconcurrencyType
set to.PrivateQueueConcurrencyType
. The above code blocks UI only whenprivateContext
'sparentContext
hasconcurrencyType
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:
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:
- What are the "standard" messages?
- Can we set properties of a managed object which is fetched inside
performBlock
* API of a context outsideperformBlock
*? - Why
performBlockAndWait
is misbehaving and causing UI block in my test code.
TEST CODE: You can download the test code from here.
回答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
orperformBlockAndWait
. The only calls that are allowed on a private context outside of the block isinit
andsetParentContext
. Anything else should be done in a block.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.
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).
回答2:
- 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
.
- 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.
- 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.
来源:https://stackoverflow.com/questions/34563690/core-data-privatequeue-performblockandwait-deadlock-while-accessing-relationship