最近在项目中又遇到了一个死锁问题,跟NSNotificationCenter有关,这个类平时不知道大家有没有注意过,postNotication的时候是加锁的,如果是在主线程注册对应的监听方法,而此时主线程又在等待另一个线程,则会导致发送通知的线程阻塞。如果发送通知的线程跟等待的线程是同一个,就会导致死锁。
具体情况是这样的,我在项目中使用了libevent库,他有两种运行模式:同步和异步。 如果是同步,则所有操作都在一个线程上执行,如果是异步,则在不同线程上执行,为了避免多线程同时访问同一个导致的崩溃,我们申明了对内部线程进行加锁,即evthread_use_pthreads。
然后我们项目里面申明了一个通知kP2PConnectStatusChangeNoti, 这个通知的回调方法是在MGCameraCollectionViewCell的awakeFromNib方法中注册的(主线程),作用是监听P2P连接的状态,显示对应的UI
然后通知发送的代码是在libevent的一个回调方法session_connect_success中,这个回调方法在p2p连接建立成功后执行
我在离开某个页面的时候,会触发destroy方法,这个方法在主线程执行,然后内部会调用libevent的某些方法,这时候问题来了,当我反复操作离开页面,进入页面(会触发p2p连接的建立)多次后,就出现了死锁。见下图,thread 1 在等待libevent释放锁,livevent在等待thread21 的session_connect_success方法执行完成,而thread21在调用setConnectStatus时,发送kP2PConnectStatusChangeNoti对线程进行了加锁,这个锁是NSNotificationCenter内部加的,我没有具体源码,但是猜测应该是锁住了主线程。 为什么这么说呢? 因为从断点时各线程的情况来看,只有thread1 (主线程)和thread21(libevent线程)在wait,其他线程都没有wait,显然是互相等待导致了死锁。 这里需要注意一点,libevent持有的锁跟NSnotificationCenter内部的锁不是同一个。
我们尝试在主线程异步发送kP2PConnectStatusChangeNoti通知,看还会不会死锁。经过实验证明,这样确实不会死锁,因为是异步的,libevent线程不会被主线程阻塞,也就能顺利的完成event的释放,从而主线程也能顺利运行下去。
那为什么NSNotificationCenter要对postNotification加锁呢?我猜我想应该是为了避免发送通知时,有其他线程对观察者进行添加和移除吧,那样会导致一些意想不到的事情发生,或者是为了避免多线程同时操作内部队列引发数据不一致的问题。我觉得前一种可能性更大,同时addObserver方法和removeObserver内部应该也进行了加锁,这样才能更好的处理多线程同时操作。如果是这样的话,就可以解释为什么postNotification会阻塞主线程了,因为在频繁进入和离开页面的时候,频繁调用了addObserver和removeObserver方法,他们主线程上加了锁(严格来说,removeObserver不一定是在主线程调用,因为这里是编译器自己加上去的,我们并没有显示调用),而postNotification在等待他们释放锁,间接引起了发送通知的线程阻塞。
由此,我们可以得出结论,使用NSNotificationCenter时,发送通知和给通知添加观察者的线程最好是同一个,这样可以避免加锁导致的线程阻塞和App卡死。