自旋锁的本质是持续占有cpu,直到获取到资源。与其余锁的忙等待的实现机制不一样。html
昨天有位开发者在 Github 上给我提了一个 issue,里面指出 OSSpinLock 在新版 iOS 中已经不能再保证安全了,并提供了几个相关资料的连接。我仔细查了一下相关资料,确认了这个让人不爽的 bug。git
2015-12-14 那天,swift-dev 邮件列表里有人在讨论 weak 属性的线程安全问题,其中有几位苹果工程师透露了自旋锁的 bug,对话内容大体以下:github
新版 iOS 中,系统维护了 5 个不一样的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。算法
具体来讲,若是一个低优先级的线程得到锁并访问共享资源,这时一个高优先级的线程也尝试得到这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程没法与高优先级线程争夺 CPU 时间,从而致使任务迟迟完不成、没法释放 lock。这并不仅是理论上的问题,libobjc 已经遇到了不少次这个问题了,因而苹果的工程师停用了 OSSpinLock。swift
苹果工程师 Greg Parker 提到,对于这个问题,一种解决方案是用 truly unbounded backoff 算法,这能避免 livelock 问题,但若是系统负载高时,它仍有可能将高优先级的线程阻塞数十秒之久;另外一种方案是使用 handoff lock 算法,这也是 libobjc 目前正在使用的。锁的持有者会把线程 ID 保存到锁内部,锁的等待者会临时贡献出它的优先级来避免优先级反转的问题。理论上这种模式会在比较复杂的多锁条件下产生问题,但实践上目前还一切都好。安全
libobjc 里用的是 Mach 内核的 thread_switch() 而后传递了一个 mach thread port 来避免优先级反转,另外它还用了一个私有的参数选项,因此开发者没法本身实现这个锁。另外一方面,因为二进制兼容问题,OSSpinLock 也不能有改动。app
最终的结论就是,除非开发者能保证访问锁的线程所有都处于同一优先级,不然 iOS 系统中全部类型的自旋锁都不能再使用了。ide
为了找到一个替代方案,我作了一个简单的性能测试,对比了一下几种可以替代 OSSpinLock 锁的性能。测试是在 iPhone6、iOS9 上跑的,代码在这里。我尝试了不一样的循环次数,结果并不都同样,我猜这多是与 CPU Cache 有关,因此这个结果只能看成一个定性分析。post
能够看到除了 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