自旋鎖的本質是持續佔有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