Notification与多线程

前几天与同事讨论到Notification在多线程下的转发问题,所以就此整理一下。

先来看看官方的文档,是这样写的:

翻译过来是:

也就是说,Notification的发送与接收处理都是在同一个线程中。为了说明这一点,我们先来看一个示例:

代码清单1:Notification的发送与处理

其输出结果如下:

可以看到,虽然我们在主线程中注册了通知的观察者,但在全局队列中post的Notification,并不是在主线程处理的。所以,这时候就需要注意,如果我们想在回调中处理与UI相关的操作,需要确保是在主线程中执行回调。

这时,就有一个问题了,如果我们的Notification是在二级线程中post的,如何能在主线程中对这个Notification进行处理呢?或者换个提法,如果我们希望一个Notification的post线程与转发线程不是同一个线程,应该怎么办呢?我们看看官方文档是怎么说的:

这里讲到了“重定向”,就是我们在Notification所在的默认线程中捕获这些分发的通知,然后将其重定向到指定的线程中。

一种重定向的实现思路是自定义一个通知队列(注意,不是NSNotificationQueue对象,而是一个数组),让这个队列去维护那些我们需要重定向的Notification。我们仍然是像平常一样去注册一个通知的观察者,当Notification来了时,先看看post这个Notification的线程是不是我们所期望的线程,如果不是,则将这个Notification存储到我们的队列中,并发送一个信号(signal)到期望的线程中,来告诉这个线程需要处理一个Notification。指定的线程在收到信号后,将Notification从队列中移除,并进行处理。

官方文档已经给出了示例代码,在此借用一下,以测试实际结果:

代码清单2:在不同线程中post和转发一个Notification

运行后,其输出如下:

可以看到,我们在全局dispatch队列中抛出的Notification,如愿地在主线程中接收到了。

这种实现方式的具体解析及其局限性大家可以参考官方文档Delivering Notifications To Particular Threads,在此不多做解释。当然,更好的方法可能是我们自己去子类化一个NSNotificationCenter,或者单独写一个类来处理这种转发。

NSNotificationCenter的线程安全性

苹果之所以采取通知中心在同一个线程中post和转发同一消息这一策略,应该是出于线程安全的角度来考量的。官方文档告诉我们,NSNotificationCenter是一个线程安全类,我们可以在多线程环境下使用同一个NSNotificationCenter对象而不需要加锁。原文在Threading Programming Guide中,具体如下:

我们可以在任何线程中添加/删除通知的观察者,也可以在任何线程中post一个通知。

NSNotificationCenter在线程安全性方面已经做了不少工作了,那是否意味着我们可以高枕无忧了呢?再回过头来看看第一个例子,我们稍微改造一下,一点一点来:

代码清单3:NSNotificationCenter的通用模式

上面的代码就是我们通常所做的事情:添加一个通知监听者,定义一个回调,并在所属对象释放时移除监听者;然后在程序的某个地方post一个通知。简单明了,如果这一切都是发生在一个线程里面,或者至少dealloc方法是在-postNotificationName:的线程中运行的(注意:NSNotification的post和转发是同步的),那么都OK,没有线程安全问题。但如果dealloc方法和-postNotificationName:方法不在同一个线程中运行时,会出现什么问题呢?

我们再改造一下上面的代码:

代码清单4:NSNotificationCenter引发的线程安全问题

这段代码是在主线程添加了一个TEST_NOTIFICATION通知的监听者,并在主线程中将其移除,而我们的NSNotification是在后台线程中post的。在通知处理函数中,我们让回调所在的线程睡眠1秒钟,然后再去设置属性i值。这时会发生什么呢?我们先来看看输出结果:

经典的内存错误,程序崩溃了。其实从输出结果中,我们就可以看到到底是发生了什么事。我们简要描述一下:

  1. 当我们注册一个观察者是,通知中心会持有观察者的一个弱引用,来确保观察者是可用的。
  2. 主线程调用dealloc操作会让Observer对象的引用计数减为0,这时对象会被释放掉。
  3. 后台线程发送一个通知,如果此时Observer还未被释放,则会向其转发消息,并执行回调方法。而如果在回调执行的过程中对象被释放了,就会出现上面的问题。

当然,上面这个例子是故意而为之,但不排除在实际编码中会遇到类似的问题。虽然NSNotificationCenter是线程安全的,但并不意味着我们在使用时就可以保证线程安全的,如果稍不注意,还是会出现线程问题。

那我们该怎么做呢?这里有一些好的建议:

  1. 尽量在一个线程中处理通知相关的操作,大部分情况下,这样做都能确保通知的正常工作。不过,我们无法确定到底会在哪个线程中调用dealloc方法,所以这一点还是比较困难。
  2. 注册监听都时,使用基于block的API。这样我们在block还要继续调用self的属性或方法,就可以通过weak-strong的方式来处理。具体大家可以改造下上面的代码试试是什么效果。
  3. 使用带有安全生命周期的对象,这一点对象单例对象来说再合适不过了,在应用的整个生命周期都不会被释放。
  4. 使用代理。

小结

NSNotificationCenter虽然是线程安全的,但不要被这个事实所误导。在涉及到多线程时,我们还是需要多加小心,避免出现上面的线程问题。想进一步了解的话,可以查看Observers and Thread Safety

参考

  1. Notification Programming Topics
  2. Threading Programming Guide
  3. NSNotification的几点说明
  4. NSNotificationCenter is thread-safe NOT
  5. Observers and Thread Safety
1 1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部