昨天有位开发者在 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 上跑的,代码在这里。这里只是测试了单线程的情况,不能反映多线程下的实际性能,所以这个结果只能当作一个定性分析。
可以看到除了 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/protobuf 内部的 spinlock 被全部替换为 dispatch_semaphore,详情可以看这个提交:https://github.com/google/protobuf/pull/1060。用 dispatch_semaphore 而不用 pthread_mutex 应该是出于性能考虑。
相关链接
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://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 是安全的。
dispatch_semaphore应该是安全的,因为它不是自旋锁,会主动让出时间片。 :twisted: :twisted:
自旋锁不阻塞线程,不会让出时间片。
OSSpinLock自旋锁会出现busy-wait状态,不会让出时间片从而一直占用CPU资源。另外pthread_mutex苹果已经作出了优化,性能不一定比dispatch_semaphore差,而且肯定是安全的。如果是iOS10以上的,完全可以用o s_unfair_lock取代OSSpinLock,等待os_unfair_lock锁的线程会处于休眠状态,从用户态切换到内核态,而并非忙等。
请问这对YYKit中很多线程安全的轮子会有影响吗 :arrow:
你可以在Github上看到相关的issue…博主已经把自旋锁替换了
:smile: 请教一下,一个什么样的学习过程才能像你们一样在技术上有不错的造诣,至少对于我来说吧。年龄比你大点,但是技术上和你比感觉差远了。望指点一二!盼复,谢谢!
你好大侠,有个问题想请教一下,以前遇到同步问题的时候
我习惯用串行的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 的线程并不会因为获得不了锁而被挂起,从而导致持有锁的低优先级线程无法释放锁。
谢谢分享。
:mrgreen: 评论
你好,如果OSSpinLock不在线程安全,那比如苹果runloop源码里还是使用OSSpinLock,那会不会有问题
大神钢琴app后来有进展么,MIDI方面还有研究么?
首先,你比较的图里面的不同的方案,实际上是不同的适用范围。
1.spinlock适用于开发硬件驱动,比如USB外接设备的软件,因为需要低级(内核级)的锁来控制设备的传输状态同步等。
2.dispatch_semaphore和mutex适合服务器软件需要高并发模型的网络应用开发,但是据我了解,Mac OS信号量做得不是很好,signal很容易crash,要set sigpipe。
3.所有的general target(一般目标)应用软件,用NSQueue是最理想的,也是它设计的初衷,为绝大多数Cocoa应用布置多线程。
4.@synchronized是:当需要自行控制锁的时候,而NSQueue不够自己的需求,然后需要手动控制的情况。
不要觉得什么都要用低级的才觉得bigger很高,要根据业务需求,如果本身对lock需求不高,只是做UI应用层业务开发的去使用spin,本身操作系统、硬件层面知识不足的人(这些人一般是软件工程师而不是硬件工程师)很容易犯错,导致死锁,实际上所有的锁都是安全的。
spin出问题也是出在开发者错误的逻辑,也是建立在不符自身领域范围的控制欲。区分优先级也是为了避免越权,有些开发者为了满足自己特殊的癖好或者虚荣心才用无法master的工具,甚至跨越上一层runtime级别的操作,很容易会开发出buggy的应用。
这也是编译器出现的目的,不至于你使用指令集级别去写应用程序,而是一层一层的编译,你要是为了bigger用Assembly Language(汇编)去写iOS应用也是很无聊的。
关于各种锁的比较,可以看一下这篇文章:
https://bestswifter.com/ios-lock/
前段时间,有朋友问到blick在什么情况下会导致死锁。你这边有什么看法吗?请赐教
大神在了解runtime. 有几个困惑。 编译器和运行时的关系? oc代码是怎么翻译成runtime的代码的? runtime是怎么转换成机器指令的?
你好,请问一下OSMemoryBarrier()的效率似乎比dispatch_semaphore_t的更高,为什么不用前者来替代OSSpinLink呢?
被前辈折服,更没有想到前辈会是YYKit的作者,二次折服. 希望可以达到前辈的高度
您好,最近我在做图文混排,遇到了一些问题:
关于textview 中排除路径exclusionPaths,
当图片在 换行空白区域移动时 不会影响图片下面的文本布局,请问您是怎么处理的。希望您有时间能给我解答一下,我看了yykit源码没有看懂。
博客好久没更新了额,最近都干吗去了呢?
hi,还在有关注这个博客吗? 有个问题希望能解惑一下,关于您yytext里面的竖排版,我如何在绘制排版之前,提前算好,需要显示的宽高呢,希望您能解答一下。
想在这里问问作者关于YYLabel的问题, 使用yy_setBackgroundColor:range:方法时,为啥range范围内的文字一旦换行了, 第二行的文字就没了背景色? 当range的location是从0开始的, 为啥整段range范围内的文字就没背景色了?
你好,能不能分享一些ios方面学习的路程,我三年还是水平还是一般般,很渴望得到回复
出个2.0不知道安不安全
大佬这边在测试中,发现在循环遍历加锁的时候,使用信号量反而性能没有NSLock 好,这里能帮忙解答下呢