My new project: Tact, a simple chat app.

Why NSNotificationCenter sometimes deadlocks when trying to deliver notifications

June 16, 2014

I’m working on something that involves some multi-threading with Grand Central Dispatch and multiple queues. Always a fun topic and a can of worms. Today, I hit a bug around this area that made me pause and think for a bit. I’m happy with myself that I was able to reason through the situation, and thought I’d post the writeup.

I’m synchronizing access to a resource using a particular serial queue. Any thread or queue posts stuff it wants to do with the resource on this queue, and it’s nicely serialized.

At one point, though, I hit a deadlock with NSNotificationCenter. Multiple queues were seemingly waiting on the lock, and the synchronization queue was blocked on posting a NSNotification. Wait, what?

When I was less experienced, these kinds of concurrency bugs used to freak me out and I didn’t really know what to do. I’d try random things and hope it worked, and sometimes it did, but I never used to develop a conclusive solution and explanation. But I guess I‘ve been doing this for a while now, and today I was able to resolve this with hypothesis-driven methodic approach. So let’s drill down. In the end, it was actually quite an easy fix. Silly, even.

One of the things that the serial queue does is post a notification about something that changed. The queue doesn’t redirect this posting anywhere else, i.e it ends up being posted on the same queue. But the posting call was blocked.

Well, let’s look at who’s consuming the notification. The only consumer was in a view controller:

someObserver = [[NSNotificationCenter defaultCenter] addObserverForName:notificationName
	object:nil
	queue:[NSOperationQueue mainQueue]
	usingBlock:^(NSNotification *note) {
		[weakSelf doSomethingInUi];
	}];

What’s going on in the main queue? The debugger shows that it is waiting to post some code in the same serial queue who’s trying to post the notification. From here on, it was easy.

So, the situation was that the serial queue was trying to post something on the main queue, while the main queue was blocked trying to post in the serial queue, and wouldn’t accept the notification. Classic deadlock.

Breaking the deadlock is easy. The technique of having notifications delivered on the main queue with the above API is usually useful, because it means you don’t have to redirect the execution yourself to another queue, but in this case it is hurting us, because the main queue may be blocked. So we’ll need to do a bit of manual legwork to more directly redirect the execution.

Also, from a requirements perspective, since the effect of the notification is that it shows some feedback to the user in the UI, it doesn’t have to execute instantly, together with the actual change. Well, it has to be fast enough for the user to perceive it as instant, but it definitely doesn’t have to happen during the same runloop.

So, let’s put the pieces together and fix the notification consumer.

someObserver = [[NSNotificationCenter defaultCenter] addObserverForName:notificationName
	object:nil
	queue:nil
	usingBlock:^(NSNotification *note) {
 		dispatch_async(dispatch_get_main_queue(), ^{
			[weakSelf doSomethingInUi];
		});
	}];

Using nil for queue means “use whatever queue is sending this notification”, which in my case was the serial synchronization queue. And we then schedule the callback code ourselves to be execute asynchronously.

And voila, the main queue is not blocked any more and this worked fine.