swift 鎖

https://www.mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.htmlhtml

https://swift.gg/2018/06/07/friday-qa-2015-02-06-locks-thread-safety-and-swift/python

 

在 Swift 中有個有趣的現象:它沒有與線程相關的語法,也沒有明確的互斥鎖/鎖(mutexes/locks)概念,甚至 Objective-C 中有的 @synchronized 和原子屬性它都沒有。幸運的是,蘋果系統的 API 能夠很是容易地應用到 Swift 中。今天,我會介紹這些 API 的用法以及從 Objective-C 過渡的一些問題,這些靈感都來源於 Cameron Pulsford。swift

快速回顧一下鎖

鎖(lock)或者互斥鎖(mutex)是一種結構,用來保證一段代碼在同一時刻只有一個線程執行。它們一般被用來保證多線程訪問同一可變數據結構時的數據一致性。主要有下面幾種鎖:安全

  • 阻塞鎖(Blocking locks):常見的表現形式是當前線程會進入休眠,直到被其餘線程釋放。
  • 自旋鎖(Spinlocks):使用一個循環不斷地檢查鎖是否被釋放。若是等待狀況不多話這種鎖是很是高效的,相反,等待狀況很是多的狀況下會浪費 CPU 時間。
  • 讀寫鎖(Reader/writer locks):容許多個讀線程同時進入一段代碼,但當寫線程獲取鎖時,其餘線程(包括讀取器)只能等待。這是很是有用的,由於大多數數據結構讀取時是線程安全的,但當其餘線程邊讀邊寫時就不安全了。
  • 遞歸鎖(Recursive locks):容許單個線程屢次獲取相同的鎖。非遞歸鎖被同一線程重複獲取時可能會致使死鎖、崩潰或其餘錯誤行爲。

APIs

蘋果提供了一系列不一樣的鎖 API,下面列出了其中一些:數據結構

  • pthread_mutex_t
  • pthread_rwlock_t
  • dispatch_queue_t
  • NSOperationQueue 當配置爲 serial 時
  • NSLock
  • OSSpinLock

除此以外,Objective-C 提供了 @synchronized 語法結構,它其實就是封裝了 pthread_mutex_t 。與其餘 API 不一樣的是,@synchronized 並未使用專門的鎖對象,它能夠將任意 Objective-C 對象視爲鎖。@synchronized(someObject)區域會阻止其餘 @synchronized(someObject) 區域訪問同一對象指針。不一樣的 API 有不一樣的行爲和能力:多線程

  • pthread_mutex_t 是一個可選擇性地配置爲遞歸鎖的阻塞鎖;
  • pthread_rwlock_t 是一個阻塞讀寫鎖;
  • dispatch_queue_t 能夠用做阻塞鎖,也能夠經過使用 barrier block 配置一個同步隊列做爲讀寫鎖,還支持異步執行加鎖代碼;
  • NSOperationQueue 能夠用做阻塞鎖。與 dispatch_queue_t 同樣,支持異步執行加鎖代碼。
  • NSLock 是 Objective-C 類的阻塞鎖,它的同伴類 NSRecursiveLock 是遞歸鎖。
  • OSSpinLock 顧名思義,是一個自旋鎖。

最後,@synchronized 是一個阻塞遞歸鎖。閉包

值類型

注意,pthread_mutex_tpthread_rwlock_t 和 OSSpinLock 是值類型,而不是引用類型。這意味着若是你用 =進行賦值操做,實際上會複製一個副本。這會形成嚴重的後果,由於這些類型沒法複製!若是你不當心複製了它們中的任意一個,這個副本沒法使用,若是使用可能會直接崩潰。這些類型的 pthread 函數會假定它們的內存地址與初始化時同樣,所以若是將它們移動到其餘地方就可能會出問題。OSSpinLock 不會崩潰,但複製操做會生成一個徹底獨立的鎖,這不是你想要的。併發

若是使用這些類型,就必須注意不要去複製它們,不管是顯式的使用 = 操做符仍是隱式地操做。
例如,將它們嵌入到結構中或在閉包中捕獲它們。框架

另外,因爲鎖本質上是可變對象,須要用 var 來聲明它們。異步

其餘鎖都是是引用類型,它們能夠隨意傳遞,而且能夠用 let 聲明。

初始化

2015-02-10 更新:本節中所描述的問題已經以驚人的速度被淘汰。蘋果昨天發佈了 Xcode 6.3 beta 1,其中包括 Swift 1.2。在其餘更改中,如今使用一個空的初始化器導入 C 結構,將全部字段設置爲零。簡而言之,你如今能夠直接使用 pthread_mutex_t(),不須要下面提到的擴展。

pthread 類型很難在 swift 中使用。它們被定義爲不透明的結構體中包含了一堆存儲變量,例如:

struct _opaque_pthread_mutex_t {
long __sig;
char __opaque[__PTHREAD_MUTEX_SIZE__];
};

目的是聲明它們,而後使用 init 函數對它們進行初始化,使用一個指針存儲和填充。在 C 中,它看起來像:

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

這段代碼能夠正常的工做,只要你記得調用 pthread_mutex_init。然而,Swift 真的真的不喜歡未初始化的變量。與上面代碼等效的 Swift 版本沒法編譯:

var mutex: pthread_mutex_t
pthread_mutex_init(&mutex, nil)
// error: address of variable 'mutex' taken before it is initialized

Swift 須要變量在使用前初始化。pthread_mutex_init 不使用傳入的變量的值,只是重寫它,可是 Swift 不知道,所以它產生了一個錯誤。爲了知足編譯器,變量須要用某種東西初始化。在類型以後使用 (),但這樣寫仍然會報錯:

var mutex = pthread_mutex_t()
// error: missing argument for parameter '__sig' in call

Swift 須要那些不透明字段的值。__sig 能夠傳入零,可是 __opaque 就有點煩人了。下面的結構體須要橋接到 swift 中:

struct _opaque_pthread_mutex_t {
var __sig: Int
var __opaque: (Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8)
}

目前沒有簡單的方法使用一堆 0 構建一個元組,只能像下面這樣把全部的 0 都寫出來:

var mutex = pthread_mutex_t(__sig: 0,
__opaque: (0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0))

這麼寫太難看了,但我沒找到好的方法。我能想到最好的作法就是把它寫到一個擴展中,這樣直接使用空的 () 就能夠了。下面是我寫的兩個擴展:

extension pthread_mutex_t {
init() {
__sig = 0
__opaque = (0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0)
}
}

extension pthread_rwlock_t {
init() {
__sig = 0
__opaque = (0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0)
}
}

能夠經過下面這種方式使用:

var mutex = pthread_mutex_t()
pthread_mutex_init(&mutex, nil)

鎖的封裝

爲了使這些不一樣的 API 更易於使用,我編寫了一系列小型函數。我決定把 with 做爲一個方便、簡短、看起來像語法的名字,靈感來自 python 的 with 聲明。Swift 函數重載容許不一樣類型使用相同的名稱。基本形式以下所示:

func with(lock: SomeLockType, f: Void -> Void) { ...

而後在鎖定的狀況下執行函數 f。下面咱們來實現這些類型。

對於值類型,它須要一個指向鎖的指針,以便 lock/unlock 函數能夠修改它。這個實現pthread_mutex_t 只是調用相應的 lock 和 unlock 函數,f 函數在二者之間調用:

func with(mutex: UnsafeMutablePointer<pthread_mutex_t>, f: Void -> Void) {
pthread_mutex_lock(mutex)
f()
pthread_mutex_unlock(mutex)
}

pthread_rwlock_t 的實現幾乎徹底相同:

func with(rwlock: UnsafeMutablePointer<pthread_rwlock_t>, f: Void -> Void) {
pthread_rwlock_rdlock(rwlock)
f()
pthread_rwlock_unlock(rwlock)
}

與讀寫鎖作個對比,它們看起來很像:

func with_write(rwlock: UnsafeMutablePointer<pthread_rwlock_t>, f: Void -> Void) {
pthread_rwlock_wrlock(rwlock)
f()
pthread_rwlock_unlock(rwlock)
}

dispatch_queue_t 更簡單。它只須要封裝 dispatch_sync:

func with(queue: dispatch_queue_t, f: Void -> Void) {
dispatch_sync(queue, f)
}

若是一我的想顯擺本身很聰明,那麼能夠充分利用 Swift 的函數式特性簡單的寫出這樣的代碼:

let with = dispatch_sync

這種寫法存在一些問題,最大的問題是它會和咱們這裏使用的基於類型的重載混淆。

NSOperationQueue 在概念上是類似的,不過沒有 dispatch_sync 能夠用。咱們須要建立一個操做(operation),將其添加到隊列中,並顯式等待它完成:

func with(opQ: NSOperationQueue, f: Void -> Void) {
let op = NSBlockOperation(f)
opQ.addOperation(op)
op.waitUntilFinished()
}

實現 NSLock 看起來像 pthread 版本,只是鎖定調用有些不一樣:

func with(lock: NSLock, f: Void -> Void) {
lock.lock()
f()
lock.unlock()
}

最後,OSSpinLock 的實現一樣也是如此:

func with(spinlock: UnsafeMutablePointer<OSSpinLock>, f: Void -> Void) {
OSSpinLockLock(spinlock)
f()
OSSpinLockUnlock(spinlock)
}

模仿 @synchronized

有了上面的封裝,模仿 @synchronized 的實現就變得很簡單。給你的類添加一個屬性,持有一個鎖,而後使用 with 替代 @synchronized :

let queue = dispatch_queue_create("com.example.myqueue", nil)

func setEntryForKey(key: Key, entry: Entry) {
with(queue) {
entries[key] = entry
}
}

從 block 中獲取數據比較麻煩。@synchronized 能夠從內部 return ,可是 with 作不到。你必須使用一個 var 變量在 block 內部賦值給它:

func entryForKey(key: Key) -> Entry? {
var result: Entry?
with(queue) {
result = entries[key]
}
return result
}

按理說能夠將這段代碼當作模板封裝在一個通用函數中,可是它沒法經過 Swift 編譯器的類型推斷,目前尚未找到解決方法。

模擬原子屬性

原子屬性(atomic)並不經常使用。與其餘代碼屬性不一樣,原子屬性並不支持組合率。若是函數 f 不存在內存泄漏,函數 g 不存在內存泄漏,那麼函數 h 只是調用 f 和 g 也不會存在內存泄漏。可是原子屬性並不知足這個條件。舉一個例子,假設你有一個定義成原子屬性而且線程安全的 Account 類:

let checkingAccount = Account(amount: 100)
let savingsAccount = Account(amount: 0)

如今要把錢轉到儲蓄帳戶中:

checkingAccount.withDraw(100)
savingsAccount.deposit(100)

在另外一個線程中,統計並顯示餘額:

println("Your total balance is: \(checkingAccount.amount + savingsAccount.amount)")

在某些狀況下,這段代碼會打印 0,而不是 100,儘管事實上這些 Account 對象自己是原子屬性,而且用戶確實有 100 的餘額。因此,最好讓整個子系統都知足原子性,而不是單個屬性。

在極少數狀況下,原子屬性是有用的,由於它並不依賴其餘特性,只須要線程安全便可。要在 Swift 中實現這一點,須要一個計算屬性來完成鎖定,用另外一個常規屬性保存值:

private let queue = dispatch_queue_create("...", nil)
private var _myPropertyStorage: SomeType

var myProperty: SomeType {
get {
var result: SomeType?
with(queue) {
result = _myPropertyStorage
}
return result!
}
set {
with(queue) {
_myPropertyStorage = newValue
}
}
}

如何選擇鎖 API

pthread API 在 Swift 中不太好用,並且功能並不比其它 API 多。通常我比較喜歡在 C 和 Objective-C 中使用它們,由於它們又好用又高效。可是在 Swift 中,除非必要,我通常不會用。

通常來講不須要用讀寫鎖,大多數狀況下讀寫速度都很是快。讀寫鎖帶來的額外開銷超過了併發讀取帶來的效率提高。

遞歸鎖會發生死鎖。多數狀況下它們是有用的,但若是你發現本身須要獲取一個已經在當前線程被鎖住的鎖,那最好從新設計代碼,一般來講不會出現這種需求。

個人建議是,若是不知道該用什麼,那就默認選擇 dispatch_queue_t 。雖然用起來相對麻煩,可是不會產生太多問題。該 API 很是方便,而且確保你永遠不會忘記調用 lock 和 unlock。它提供了許多有用的 API,如使用單個 dispatch_async在後臺執行被鎖定的代碼,或者設置定時器或其餘做用於 queue 的事件源,以便它們自動執行鎖定。你甚至能夠用它做爲 NSNotificationCenter 觀察者,或者使用 NSOperationQueue 的屬性 underlyingQueue 做爲 NSURLSession 代理。

NSOperationQueue 可能認爲本身和 dispatch_queue_t 同樣牛👃,可是實際上不多有場景須要使用它。這個 API 使用起來更麻煩,並且和其餘 API 比沒有什麼優點,無非在某些狀況下,它能自動進行操做的依賴關係管理,也就這點比較有用。

NSLock 是一個簡單的鎖定類,易於使用且效率很高。若是須要顯式鎖定和解鎖,那能夠用它替代 dispatch_queue_t 。但在大多數狀況下不須要使用它。

OSSpinLock 對於常用鎖定、競爭較少且鎖定代碼運行速度快的用戶來講,是一個很好的選擇。它的開銷很是少,有助於提高性能。若是代碼可能會在很長一段時間內保持鎖定或競爭不少,那最好不要用這個 API,由於這會浪費 CPU 時間。一般來講,你能夠先使用 dispatch_queue_t ,若是這塊出現了性能問題,再考慮換成 OSSpinLock 。

總結

Swift 語言層面並不支持線程同步,可是 Apple 的系統框架有不少好用的 API。GCD 和 dispatch_queue_t 很是好用,而且Swift 中的 API 也是如此。雖然 Swift 裏沒有 @synchronized 和原子屬性,但咱們有其餘更好的選擇。

相關文章
相關標籤/搜索