構建一個 @synchronized

原文連接:https://swift.gg/2018/07/30/friday-qa-2015-02-20-lets-build-synchronized/
做者:Mike Ash
譯者:Sunnyyoung
校對:智多芯
定稿:numbbbbbCMB
編程

上一篇文章講了線程安全,今天這篇最新一期的 Let's Build 我會探討一下如何實現 Objective-C 中的 @synchronized。本文基於 Swift 實現,Objective-C 版本大致上也差很少。swift

回顧

@synchronized 在 Objective-C 中是一種控制結構。它接受一個對象指針做爲參數,後面跟着一段代碼塊。對象指針充當鎖,在任什麼時候候 @synchronized 代碼塊中只容許有一個線程使用該對象指針。數組

這是一種使用鎖進行多線程編程的簡單方法。舉個例子,你可使用 NSLock 來保護對 NSMutableArray 的操做:緩存

NSMutableArray *array;
NSLock *arrayLock;

[arrayLock lock];
[array addObject: obj];
[arrayLock unlock];
複製代碼

也可使用 @synchronized 來將數組自己加鎖:安全

@synchronized(array) {
    [array addObject: obj];
}
複製代碼

我我的更喜歡顯式的鎖,這樣作既可使事情更清楚,@synchronized 的性能沒那麼好,緣由以下圖所示。但它(@synchronized)使用很方便,無論怎樣,實現起來都頗有意思。數據結構

原理

Swift 版本的 @synchronized 是一個函數。它接受一個對象和一個閉包,並使用持有的鎖調用閉包:多線程

func synchronized(obj: AnyObject, f: Void -> Void) {
    ...
}
複製代碼

問題是,如何將任意對象變成鎖?閉包

在一個理想的世界裏(從實現這個函數的角度來看),每一個對象都會爲鎖留出一些額外空間。在這個額外的小空間裏 synchronized 可使用適當的 lockunlock 方法。然而實際上並無這種額外空間。這多是件好事,由於這會增大對象佔用的內存空間,可是大多數對象永遠都不會用到這個特性。app

另外一種方法是用一張表來記錄對象到鎖的映射。synchronized 能夠查找表中的鎖,而後執行 lockunlock 操做。這種方法的問題是表自己須要保證線程安全,它要麼須要本身的鎖,要麼須要某種特殊的無鎖數據結構。爲表單獨設置一個鎖要容易得多。函數

爲了防止鎖不斷累積常駐,表須要跟蹤鎖的使用狀況,並在再也不須要鎖的時候銷燬或者複用。

實現

要實現將對象映射到鎖的表,NSMapTable 很是合適。它能夠把原始對象的地址設置成鍵(key),而且能夠保存對鍵(key)和值(value)的弱引用,從而容許系統自動回收未被使用的鎖。

let locksTable = NSMapTable.weakToWeakObjectsMapTable()
複製代碼

表中存儲的對象是 NSRecursiveLock 實例。由於它是一個類,因此能夠直接用在 NSMapTable 中,這點 pthread_mutex_t 就作不到。@synchronized 支持遞歸語義,咱們的實現同樣支持。

表自己也須要一個鎖。自旋鎖(spinlock)在這種狀況下很適合使用,由於對錶的訪問是短暫的:

var locksTableLock = OS_SPINLOCK_INIT
複製代碼

有了這個表,咱們就能夠實現如下方法:

func synchronized(obj: AnyObject, f: Void -> Void) {
複製代碼

它所作的第一件事就是在 locksTable 中找出與 obj 對應的鎖,執行操做以前必須持有 locksTableLock 鎖:

OSSpinLockLock(&locksTableLock)
var lock = locksTable.objectForKey(obj) as! NSRecursiveLock?
複製代碼

若是表中沒有找到對應鎖,則建立一個新鎖並保存到表中:

if lock == nil {
    lock = NSRecursiveLock()
    locksTable.setObject(lock!, forKey: obj)
}
複製代碼

有了鎖以後主表鎖就能夠釋放了。爲了不死鎖這必需要在調用 f 以前完成:

OSSpinLockUnlock(&locksTableLock)
複製代碼

如今咱們能夠調用 f 了,在調用先後分別進行加鎖和解鎖操做:

lock!.lock()
    f()
    lock!.unlock()
}
複製代碼

對比蘋果的方案

蘋果實現 @synchronized 的方案能夠在 Objective-C runtime 源碼中找到:

http://www.opensource.apple.com/source/objc4/objc4-646/runtime/objc-sync.mm

它的主要目標是性能,所以不像上面那個玩具般的例子那麼簡單。對比它們之間有什麼異同是一件很是有趣的事。

基本概念是相同的。存在一個全局表,它將對象指針映射到鎖,而後該鎖在 @synchronized 代碼塊先後進行加鎖解鎖操做。

對於底層的鎖對象,Apple 使用配置爲遞歸鎖的 pthread_mutex_tNSRecursiveLock 內部極可能也使用了 pthread_mutex_t,直接使用就省去了中間環節,並避免了運行時對 Foundation 的依賴。

表自己的實現是一個鏈表而不是一個哈希表。常見的狀況是在任何給定的時間裏只存在少數幾個鎖,因此鏈表的性能表現很不錯,可能比哈希表性能更好。每一個線程緩存了最近在當前線程查找的鎖,從而進一步提升性能。

蘋果的實現並非只有一個全局表,而是在一個數組裏保存了 16 個表。對象根據地址映射到不一樣的表,這減小了不一樣對象 @synchronized 操做致使的沒必要要的資源競爭,由於它們極可能使用的是兩個不一樣的全局表。

蘋果的實現沒有使用弱指針引用(這會大量增長額外開銷),而是爲每一個鎖保留一個內部的引用計數。當引用計數達到零時,該鎖能夠給新對象從新使用。未使用的鎖不會被銷燬,但複用意味着在任什麼時候間鎖的總數都不能超過激活鎖的數量,也就是說鎖的數量不隨着新對象的建立無限制增加。

蘋果的實現方案很是巧妙,性能也不錯。但與使用單獨的顯式鎖相比,它仍然會帶來一些不可避免的額外開銷。尤爲是:

  1. 若是不相關的對象恰好被分配到同一個全局表中,那麼它們仍然可能存在資源競爭。
  2. 一般狀況下在線程緩存中查找一個不存在的鎖時,必須獲取並釋放一個自旋鎖。
  3. 必須作更多的工做來查找全局表中對象的鎖。
  4. 即便不須要,每一個加鎖/解鎖週期都會產生遞歸語義方面的開銷。

結論

@synchronized 是一個有趣的語言結構,實現起來並不簡單。它的做用是實現線程安全,但它的實現自己也須要同步操做來保證線程安全。咱們使用全局鎖來保護對鎖表的訪問,蘋果的實現中則使用不一樣的技巧來提升性能。

相關文章
相關標籤/搜索