昨天有位开发者在 Github 上给我提了一个 issue,里面指出 OSSpinLock 在新版 iOS 中已经不能再保证安全了,并提供了几个相关资料的链接。我仔细查了一下相关资料,确认了这个让人不爽的 bug。
OSSpinLock 的问题
2015-12-14 那天,swift-dev 邮件列表里有人在讨论 weak 属性的线程安全问题,其中有几位苹果工程师透露了自旋锁的 bug,对话内容大致如下:
新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。
具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。
苹果工程师 Greg Parker 提到,对于这个问题,一种解决方案是用 truly unbounded backoff 算法,这能避免 livelock 问题,但如果系统负载高时,它仍有可能将高优先级的线程阻塞数十秒之久;另一种方案是使用 handoff lock 算法,这也是 libobjc 目前正在使用的。锁的持有者会把线程 ID 保存到锁内部,锁的等待者会临时贡献出它的优先级来避免优先级反转的问题。理论上这种模式会在比较复杂的多锁条件下产生问题,但实践上目前还一切都好。
libobjc 里用的是 Mach 内核的 thread_switch() 然后传递了一个 mach thread port 来避免优先级反转,另外它还用了一个私有的参数选项,所以开发者无法自己实现这个锁。另一方面,由于二进制兼容问题,OSSpinLock 也不能有改动。
最终的结论就是,除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。
OSSpinLock 的替代方案
为了找到一个替代方案,我做了一个简单的性能测试,对比了一下几种能够替代 OSSpinLock 锁的性能。测试是在 iPhone6、iOS9 上跑的,代码在这里。我尝试了不同的循环次数,结果并不都一样,我猜这可能是与 CPU Cache 有关,所以这个结果只能当作一个定性分析。
可以看到除了 OSSpinLock 外,dispatch_semaphore 和 pthread_mutex 性能是最高的。有消息称,苹果在新系统中已经优化了 pthread_mutex 的性能,所以它看上去和 OSSpinLock 差距并没有那么大了。
开源社区的反应
苹果
查看 CoreFoundation 的源码能够发现,苹果至少在 2014 年就发现了这个问题,并把 CoreFoundation 中的 spinlock 替换成了 pthread_mutex,具体变化可以查看这两个文件:CFInternal.h(855.17)、CFInternal.h(1151.16)。苹果自己发现问题后,并没有更新 OSSpinLock 的文档,也没有告知开发者,这有些让人失望。
google/protobuf 内部的 spinlock 被全部替换为 dispatch_semaphore,详情可以看这个提交:https://github.com/google/protobuf/pull/1060。用 dispatch_semaphore 而不用 pthread_mutex 应该是出于性能考虑。
其他项目
因为 OSSpinLock 出现这种问题的几率很小,也没有引起很大的重视,我所能找到的也只有 ReactiveCocoa 在讨论这个问题。
相关链接
https://lists.swift.org/pipermail/swift-dev/Week-of-Mon-20151214/000344.html
http://mjtsai.com/blog/2015/12/16/osspinlock-is-unsafe/
http://engineering.postmates.com/Spinlocks-Considered-Harmful-On-iOS/
https://twitter.com/steipete/status/676851647042203648

- […] 不再安全的 OSSpinLock:OSSpinLock 是在 iOS 多线程开发中一种常用的锁,但是最近 Apple 工程师表示 OSSpinLock 存在一个潜在的问题,有可能造成优先级反转。这篇文章对这个问题的进行了一些讨论,并定性分析了可能的替代方案的性能。虽然可以说是很少见的情况,但是如果你在项目中使用了自旋锁的话,考虑将它换成其他方案也许是明智的选择。 […]
https://lists.swift.org/pipermail/swift-dev/Week-of-Mon-20151214/000321.html
12月14
感谢分享
您好,在拜读完这篇文章后,有一个问题希望博主能够解惑。
我在多线程这块理解的不是特别到位,还望多多见谅!
Thanks。
我的问题如下:
Google 在 Protobuf 项目中因为 OSSpinLock 可能会导致 Priority Inversion,故将 OSSpinLock 替换成了 dispatch_semaphore 相关的API。那么 dispatch_semaphore 为何不会引起 Priority Inversion 呢?
苹果员工说 libobjc 里 spinlock 是用了一些私有方法 (mach_thread_switch),贡献出了高线程的优先来避免优先级反转的问题,但是我翻了下 libdispatch 的源码倒是没发现相关逻辑,也可能是我忽略了什么。。在我的一些测试中,OSSpinLock 和 dispatch_semaphore 都不会产生特别明显的死锁,所以我也无法确定用 dispatch_semaphore 代替 OSSpinLock 是否正确。能够肯定的是,用 pthread_mutex 是安全的。
请问这对YYKit中很多线程安全的轮子会有影响吗
你可以在Github上看到相关的issue...博主已经把自旋锁替换了
你好大侠,有个问题想请教一下,以前遇到同步问题的时候
我习惯用串行的GCD队列来做,现在都改成了锁,但是不知道
这两种方式的本质区别和实际的效率是怎样的?
非常期待您的回复!感谢。
为什么总觉得自己构建出来的项目比较乱呢?
文件夹是ViewControllers、Views、Models,Core-》Core里面分Categories,Helpers,Utils等。
Views里面放一些公用的View,例如输入框,封装的弹出菜单,TableViewCell等等。
Model放置一些数据,就是服务器返回回来后的一些字段,包括帐号登录的单例方法等。
Model层还好,但是Views和ViewControllers,后面实在不想再放到Views去了。直接放到Views文件夹下面,因为感觉两个分开最后好难找东西。
请求一下,大神是如何分类的。
感谢分享!我有个疑问。
OSSpinLock 的这个问题感觉跟“优先级翻转”没有关系。我的理解是出现“优先级翻转”问题的前提是,锁会导致线程被挂起,而 spin lock 并不会。比如说mutex,它会出现“优先级翻转”问题:
当有三个线程 A, B, C, 优先级 A>B>C。C 先持有一个mutex,A 后来需要拿到这个mutex,于是 A 被挂起。在C还没有释放这个mutex的时候,B就绪,因为B的优先级被C高,所以被激活运行。这就产生一个矛盾,B 比 A 优先运行,但其实 B 的优先级比 A 低。
spin lock 的问题恰恰相反,申请 spin lock 的线程并不会因为获得不了锁而被挂起,从而导致持有锁的低优先级线程无法释放锁。
谢谢分享。
你好,如果OSSpinLock不在线程安全,那比如苹果runloop源码里还是使用OSSpinLock,那会不会有问题