This year I worked on a Core Data application. All of the parts of its sync were in a single file, which as a result had too much state tracking. The result wasn’t just complicated, it was dangerous. Should a state change be missed, the operation would never complete.
I resolved this by breaking it into many
NSOperation subclasses. Because of the new design, it should have supported multithreading a large portion of the sync. But when I allowed those operations to operate concurrently (to a limit, more on that in another post), the application would get into thread deadlocks that I couldn’t understand.
Eventually, I actually read what the debugger was telling me. It turns out that my multi-threaded code was all running on a single thread. I had a Core Data context for each operation and was using private queues, surely this would be enough?
It’s not. My expectations were set wrong by misunderstanding of
performBlockAndWait: based on reading too much into
NSPrivateQueueConcurrencyType. Apparently this is something discussed in WWDC sessions, but not in Apple’s NSManagedObjectContext reference and I’d somehow missed it.
Given that I was using a managed object context with a private queue, I thought
performBlockAndWait: blocks were being executed on the private queue.
That’s not how it works.
I was straightened out by BJ Homer on Twitter:
@tewha If you use `performBlockAndWait`, it will prefer to run on the caller’s thread if possible. Just has to obtain a lock first.
In fact, calls to
performBlockAndWait: make the current thread wait until it can get Core Data concurrency locks, then executes on the current thread. This isn’t documented, but neither was my assumption of how things worked.
In my case, this was the main thread because I was executing code in AFNetworking completion blocks, which default to the main queue. As I had assumed this code would not be run from the main thread, locks here conflicted with those I’d constructed on other threads and caused the deadlock.
I fixed it by dispatching the bulk of my completion blocks to a dedicated background queue.
What does this really mean?
Be dumb about Core Data! Don’t make assumptions about which thread your code will run on. Even if you’re right, there’s no guarantees about the future.
performBlockAndWait calls your block in an environment where it can access Core Data via the target context and waits for the code to complete. You should not assume anything about the thread.
performBlock calls your block in an environment where it can access Core Data via the target context and doesn’t wait for the code to complete. You should not assume anything about the thread.