不再安全的 OSSpinLock
文摘来源:ibireme 的博客:《不再安全的 OSSpinLock 》,有增删。
YYModel
的相关 issue。
目录
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 来避免优先级反转,另外 libobjc
还用了一个私有的参数选项,所以开发者无法自己实现这个锁。另一方面,由于二进制兼容问题,OSSpinLock
也不能有改动。
最终的结论就是,除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。
OSSpinLock 的替代方案
为了找到一个替代方案,我做了一个简单的性能测试,对比了一下几种能够替代 OSSpinLock
锁的性能。测试是在 iPhone6 、iOS9 上跑的,代码在这里。这里只是测试了单线程的情况,不能反映多线程下的实际性能,所以这个结果只能当作一个定性分析。

可以看到除了 OSSpinLock
外,dispatch_semaphore
和 pthread_mutex
性能是最高的。有消息称,苹果在新系统中已经优化了 pthread_mutex
的性能,所以它看上去和 OSSpinLock
差距并没有那么大了。
社区反应
苹果
查看 CoreFoundation
的源码能够发现,苹果至少在 2014 年就发现了这个问题,并把 CoreFoundation
中的 spinlock
替换成了 pthread_mutex
,具体变化可以查看这两个文件:CFInternal.h (855.17)
、CFInternal.h (1151.16)
。苹果自己发现问题后,并没有及时更新 OSSpinLock
的文档,也没有告知开发者,这有些让人失望。
在 iOS 10 / macOS 10.12 发布时,苹果提供了新的 os_unfair_lock
作为 OSSpinLock
的替代,并且将 OSSpinLock
标记为了 Deprecated 。
Google
google/protobuf 内部的 spinlock
被全部替换为 dispatch_semaphore
,详情可以看这个提交:https://github.com/google/protobuf/pull/1060 。用 dispatch_semaphore
而不用 pthread_mutex
应该是出于性能考虑。
精选评论
dispatch_semaphore 会引起优先级反转吗?
笔者注:之前有在 Twitter 上看到有人说使用 dispatch_semaphore 当锁使用可能会引起优先级反转,当时也没看太明白。这篇文章的评论下有人和本文作者 ibireme 也讨论了这个问题,但也没有明确的结论。
Boolean93 留言:
Google 在 Protobuf
项目中因为 OSSpinLock
可能会导致 Priority Inversion ,故将 OSSpinLock
替换成了 dispatch_semaphore
相关的 API 。那么 dispatch_semaphore
为何不会引起 Priority Inversion 呢?
ibireme 回复:
苹果员工说 libobjc
里 spinlock
是用了一些私有方法 (mach_thread_switch
) ,让高优先级的线程会临时贡献出它的优先级,以避免优先级反转的问题。
但是我翻了下 libdispatch
的源码倒是没发现相关逻辑,也可能是我忽略了什么。。在我的一些测试中,OSSpinLock
和 dispatch_semaphore
都不会产生特别明显的死锁,所以我也无法确定用 dispatch_semaphore
代替 OSSpinLock
是否正确。能够肯定的是,用 pthread_mutex
是安全的。
yb坏蛋biubiu~ 回复:
OSSpinLock
自旋锁会出现 busy-wait 状态,不会让出时间片从而一直占用 CPU 资源。另外 pthread_mutex
苹果已经作出了优化,性能不一定比 dispatch_semaphore
差,而且肯定是安全的。
如果是 iOS 10 以上的,完全可以用 os_unfair_lock
取代 OSSpinLock
,等待 os_unfair_lock
锁的线程会处于休眠状态,从用户态切换到内核态,而并非忙等。
Last updated
Was this helpful?