Avoid dispatch_sync.

Working on a Mac app recently, I ran into a situation where the app was hitting a deadlock. It was difficult to diagnose, and involved an Apple support incident (which resulted in a great answer from an Apple engineer) so I wanted to share it here.

The scenario is that at application startup there was a flurry of activity that was performing many operations simultaneously on background threads, using Grand Central Dispatch to dispatch the operations, and when the operations were done, they needed to update some UI.

Here’s a simple example that boils the problem down to its essentials:

[cc lang=”objc”]
for (int i=0; i<400; i++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"Hello"); }); } [/cc] This dispatches 400 asynchronous operations that will each perform a synchronous operation on the main thread. This will instantly deadlock. So what's the problem? Two things conspire against us here. The first is that there's a limited pool of threads that Grand Central Dispatch uses; once there are a certain number of threads created, it just won't create any more until some of the operations complete. And the second is that the OS itself uses dispatch_sync on the main thread. What happens is that all of these background threads start queueing up operations and saturate the thread pool. Meanwhile, the OS, on the main thread, in the process of doing some of its own work, calls dispatch_sync, which blocks waiting for a spot to open up in the thread pool. Deadlock. This is easy to avoid: Don't use dispatch_sync to dispatch operations on the main queue. You can use it sparingly, but use dispatch_async where possible. In the program where I originally ran into this problem, the way the app ran into this problem was more real-world than this contrived example. Here's how we actually ran into this problem. The app, at startup, queried the server for some data. Once received, the app updated or created the corresponding Core Data objects that the app was managing, which triggered KVO observers. The KVO observer was accessing a property on the model objects which was implemented using NSManagedObjectContext's performBlockAndWait: which is implemented using dispatch_sync. This is a bit more awkward to avoid. It either means your Core Data access needs to be asynchronous (doable, but a bit awkward), all done on its own thread (meaning you must never forget one of those performBlockAndWait: calls, and now you're on the hook for making sure the app doesn't terminate during an asynchronous save), or simply all on the main thread. Most Core Data applications I've seen do all the database work on the main thread; it's only when you start moving work of the main thread that you have these considerations. So, once bitten twice shy, my advice is simply to avoid dispatch_sync. You can't know the state of the thread pool and if it's full, you risk a deadlock.