Need help understanding a threading issue


Rick Aurbach
 

Let me start by saying I have a solution to my problem, but would like to know why some of the other things I tried didn't work...

I have some procedures which are performed on background queues (specifically importing/exporting between a CoreData database and zipped JSON files). I want to communicate between these operations and the main queue, either to signify completion or to report an error. (I need to execute on the main queue because there is UI involved.)

My natural thought was to include the execution block in an appropriate place in an Operation's main() using

    OperationQueue.main.addOperation { [weak self] in
       ...
    }

but the the execution block was never executed. (i.e., a breakpoint on the addOperation call triggered, but a breakpoint in the execution block never triggered and the UI code in the execution block did not execute.) Replacing the OperationQueue call with DispatchQueue.main.async {...} didn't work either. The execution block was never executed.

BUT if I use NotificationCenter.default.post(...) instead, placing the operation block in

    NotificationCenter.default.addObserver(forName: <name>, object: nil, queue: OperationQueue.main) { [weak self] in
        ...
    }
then the execution block is executed and the UI code runs as expected.

While I understand why my actual solution works, I don't understand why my original attempts failed. If you could explain this to me, I would appreciate the chance to increase my understanding.

Thanks,
Rick Aurbach


 

The only reason I can think of that the first two attempts wouldn’t work is that the main thread’s runloop is blocked, so it can’t process queued blocks. Are you by chance doing something on the main queue that blocks until your background work finishes? In that case you’ve got a deadlock. (But in that case you’d also notice that your app’s UI has frozen…)

—Jens


Rick Aurbach
 

Yes, that was my thinking as well. But, the UI was not frozen. (And, indeed, if it had been frozen, I wouldn't have expected the Notification approach to work either.) 

One additional piece of information I may have inadvertently left out of my original message: using the original approach (of injecting a block into the main queue), I can get the function to work by strategically placing breakpoints in the queued background operations. I don't know whether this means that the breakpoints are interfering with how the queuing works or whether it's a sign of a timing issue or what. I did consider that there was a timing issue and tested that by calling the block with DispatchQueue.main.asyncAfter(..) using a 100ms delay, but that didn't help either.

[This reply has gotten me thinking... I wonder if this has something to do with capture semantics and the fact that the operation that does the dispatch has completed (and therefore was deleted) before the block executed. I could test this by changing the Operation to call a method of a more persistent class (and having that method issue the dispatch call). (If you're following what I'm thinking here.)

I'll try that and let the group know...

Rick


Rick Aurbach
 

Okay, I've got it.

The problem was that I was calling OperationQueue.main.addOperation(..) from an Operation. And that call was both capturing "self" and was the last piece of work in the Operation's main(). So immediately after queuing the new main-thread operation, the caller was marked finished and was deleted. So by the time the main-thread operation executed, its attempt to capture "self" failed and (like all good closures) exited safely.

SOLUTION: class A is the controller for the process. It holds the operation queue as a property and sends it the addOperations([...]) call that begins the whole background process. One Operation wants to display status (doing UI on the main thread). In the working case, it does so by calling a method in class A which (in turn) issues the appropriate OperationQueue.main.addOperation{...} call. This way, when the main-thread operation actually runs, the "self" it captures is class A (which still exists) rather than the Operation (which no longer does).