rob5408
rob5408

Reputation: 2982

waitUntilAllOperationsAreFinished and objectWithID

Update I can confirm that objectWithID could potentially need a parent (or grandparent, etc) context's thread to do some fetching so avoid blocking your parent thread using something like waitUntilAllOperationsAreFinished.

As a quick test I pointed the children moc's parent to their grandparent instead and left the children threads blocking the original parent. In this setup the deadlock never occurred. This is a poor architecture though so I'll be rearchitecting.

Original Question

I have two layers of NSOperationQueue. The first is an NSOperation graph with operations that have a set of dependencies between them. They all run fine without deadlocking each other. Within one of these operations (a Scheduler for groups of people) I have broken out its work to more discrete chunks that can be run on another NSOperationQueue. However I still will want the Scheduler to finish creating all of its schedules before the larger operation is considered finished. To that end, once I create all Schedule operations and add them to the Scheduler operation queue, I call waitUntilAllOperationsAreFinished on the operation queue. This is where I deadlock.

I am using Core Data and have an NSBlockOperation subclass called BlockOperation that handles the routine of taking a parent managed object context, creating a PrivateQueueConcurrencyType child context, calling the provided block using performBlockAndWait and finally waiting on the parent context to merge changes. Here's some code...

init(block: (NSManagedObjectContext?) -> Void, withDependencies dependencies: Array<NSOperation>, andParentManagedObjectContext parentManagedObjectContext: NSManagedObjectContext?) {
    self.privateContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)

    super.init()

    self.queuePriority = NSOperationQueuePriority.Normal
    addExecutionBlock({
        if (parentManagedObjectContext != nil) {
            self.parentContext = parentManagedObjectContext!

            self.privateContext.parentContext = parentManagedObjectContext!

            self.privateContext.performBlockAndWait({ () -> Void in
                block(self.privateContext)
            })

            self.parentContext!.performBlockAndWait({ () -> Void in
                var error: NSError?
                self.parentContext!.save(&error)
            })
        }
    })

    for operation in dependencies {
        addDependency(operation)
    }
}

This is working really well for me already. But now I want to block a calling thread until an operation queue on it has finished all of its operations. Like this...

for group in groups {
    let groupId = group.objectID
    let scheduleOperation = BlockOperation(
        block: { (managedObjectContext: NSManagedObjectContext?) -> Void in
            ScheduleOperation.scheduleGroupId(groupId, inManagedObjectContext: managedObjectContext!)
        },
        withDependencies: [],
        andParentManagedObjectContext: managedObjectContext)
    scheduleOperationQueue.addOperation(scheduleOperation)
}

scheduleOperationQueue.waitUntilAllOperationsAreFinished()

...this thread gets stuck on that last line (obviously). But we never see the other threads make any progress past a certain point. Pausing the debugger I see where the queued operations are stuck. It's in a ScheduleOperation's init method where we fetch the group using the provided id. (ScheduleOperation.scheduleGroupId calls this init)

convenience init(groupId: NSManagedObjectID, inManagedObjectContext managedObjectContext: NSManagedObjectContext) {

    let group = managedObjectContext.objectWithID(groupId) as Group
    ...

Does objectWithID need to execute code on the "parent" thread that its parent moc is associated with and therefore creating a deadlock? Is there anything else about my approach that could be causing this?

Note: Although I am writing this is Swift, I have added Objective-C as a tag because I feel like this is not a language specific issue, but a framework specific one.

Upvotes: 0

Views: 637

Answers (1)

Michał Ciuba
Michał Ciuba

Reputation: 7944

In general it's not specified on which thread objectWithID will be called, it's an implementation detail. I had some problems with Core Data deadlocks in the past (although in different circumstances) and I found out that the framework does some locking internally when you invoke methods on NSManagedObjectContext. So yes, I think it might result in a deadlock.

I have no advice other than re-designing your architecture, maybe it can be simplified a little. Keep in mind that you already have a private serial queue associated with a context, which guarantees that the operations will be called in the specified order. You can therefore share the same context between all the ScheduleOperation instances. Set scheduleOperationQueue.maxConcurrentOperationsCount to 1, so that operations will execute one after another. And instead of blocking the calling thread, call a completion handler when the last operation finishes (you can use oepration's completionBlock).

Upvotes: 1

Related Questions