iOS 中的各種鎖

發表於2016-11-28

在日常開發過程中,為了提升程式執行效率,以及使用者體驗,我們經常使用多執行緒。在使用多執行緒的過程中,難免會遇到資源競爭問題。我們採用鎖的機制來確保執行緒安全。

執行緒安全

當一個執行緒訪問資料的時候,其他的執行緒不能對其進行訪問,直到該執行緒訪問完畢。即,同一時刻,對同一個資料操作的執行緒只有一個。只有確保了這樣,才能使資料不會被其他執行緒汙染。而執行緒不安全,則是在同一時刻可以有多個執行緒對該資料進行訪問,從而得不到預期的結果。

比如寫檔案和讀檔案,當一個執行緒在寫檔案的時候,理論上來說,如果這個時候另一個執行緒來直接讀取的話,那麼得到將是不可預期的結果。

為了執行緒安全,我們可以使用鎖的機制來確保,同一時刻只有同一個執行緒來對同一個資料來源進行訪問。在開發過程中我們通常使用以下幾種鎖。

  1. NSLock
  2. NSRecursiveLock
  3. NSCondition
  4. NSConditionLock
  5. pthread_mutex
  6. pthread_rwlock
  7. POSIX Conditions
  8. OSSpinLock
  9. os_unfair_lock
  10. dispatch_semaphore
  11. @synchronized

訊號量

在多執行緒環境下用來確保程式碼不會被併發呼叫。在進入一段程式碼前,必須獲得一個訊號量,在結束程式碼前,必須釋放該訊號量,其他想要想要執行該程式碼的執行緒必須等待直到前者釋放了該訊號量。

以一個停車場的運作為例。簡單起見,假設停車場只有三個車位,一開始三個車位都是空的。這時如果同時來了五輛車,看門人允許其中三輛直接進入,然後放下車攔,剩下的車則必須在入口等待,此後來的車也都不得不在入口處等待。這時,有一輛車離開停車場,看門人得知後,開啟車攔,放入外面的一輛進去,如果又離開兩輛,則又可以放入兩輛,如此往復。
在這個停車場系統中,車位是公共資源,每輛車好比一個執行緒,看門人起的就是訊號量的作用。

互斥鎖

一種用來防止多個執行緒同一時刻對共享資源進行訪問的訊號量,它的原子性確保瞭如果一個執行緒鎖定了一個互斥量,將沒有其他執行緒在同一時間可以鎖定這個互斥量。它的唯一性確保了只有它解鎖了這個互斥量,其他執行緒才可以對其進行鎖定。當一個執行緒鎖定一個資源的時候,其他對該資源進行訪問的執行緒將會被掛起,直到該執行緒解鎖了互斥量,其他執行緒才會被喚醒,進一步才能鎖定該資源進行操作。

NSLock

NSLock實現了最基本的互斥鎖,遵循了 NSLocking 協議,通過 lockunlock 來進行鎖定和解鎖。其使用也非常簡單

由於是互斥鎖,當一個執行緒進行訪問的時候,該執行緒獲得鎖,其他執行緒進行訪問的時候,將被作業系統掛起,直到該執行緒釋放鎖,其他執行緒才能對其進行訪問,從而卻確保了執行緒安全。但是如果連續鎖定兩次,則會造成死鎖問題。那如果想在遞迴中使用鎖,那要怎麼辦呢,這就用到了 NSRecursiveLock 遞迴鎖。

NSRecursiveLock

遞迴鎖,顧名思義,可以被一個執行緒多次獲得,而不會引起死鎖。它記錄了成功獲得鎖的次數,每一次成功的獲得鎖,必須有一個配套的釋放鎖和其對應,這樣才不會引起死鎖。只有當所有的鎖被釋放之後,其他執行緒才可以獲得鎖

NSCondition

NSCondition 是一種特殊型別的鎖,通過它可以實現不同執行緒的排程。一個執行緒被某一個條件所阻塞,直到另一個執行緒滿足該條件從而傳送訊號給該執行緒使得該執行緒可以正確的執行。比如說,你可以開啟一個執行緒下載圖片,一個執行緒處理圖片。這樣的話,需要處理圖片的執行緒由於沒有圖片會阻塞,當下載執行緒下載完成之後,則滿足了需要處理圖片的執行緒的需求,這樣可以給定一個訊號,讓處理圖片的執行緒恢復執行。

NSConditionLock

NSConditionLock 物件所定義的互斥鎖可以在使得在某個條件下進行鎖定和解鎖。它和 NSCondition 很像,但實現方式是不同的。

當兩個執行緒需要特定順序執行的時候,例如生產者消費者模型,則可以使用 NSConditionLock 。當生產者執行執行的時候,消費者可以通過特定的條件獲得鎖,當生產者完成執行的時候,它將解鎖該鎖,然後把鎖的條件設定成喚醒消費者執行緒的條件。鎖定和解鎖的呼叫可以隨意組合,lockunlockWithCondition: 配合使用 lockWhenCondition:unlock 配合使用。

當生產者釋放鎖的時候,把條件設定成了1。這樣消費者可以獲得該鎖,進而執行程式,如果消費者獲得鎖的條件和生產者釋放鎖時給定的條件不一致,則消費者永遠無法獲得鎖,也不能執行程式。同樣,如果消費者釋放鎖給定的條件和生產者獲得鎖給定的條件不一致的話,則生產者也無法獲得鎖,程式也不能執行。

pthread_mutex

POSIX 互斥鎖是一種超級易用的互斥鎖,使用的時候,只需要初始化一個 pthread_mutex_tpthread_mutex_lock 來鎖定 pthread_mutex_unlock 來解鎖,當使用完成後,記得呼叫 pthread_mutex_destroy 來銷燬鎖。

pthread_rwlock

讀寫鎖,在對檔案進行操作的時候,寫操作是排他的,一旦有多個執行緒對同一個檔案進行寫操作,後果不可估量,但讀是可以的,多個執行緒讀取時沒有問題的。

  • 當讀寫鎖被一個執行緒以讀模式佔用的時候,寫操作的其他執行緒會被阻塞,讀操作的其他執行緒還可以繼續進行。
  • 當讀寫鎖被一個執行緒以寫模式佔用的時候,寫操作的其他執行緒會被阻塞,讀操作的其他執行緒也被阻塞。

POSIX Conditions

POSIX 條件鎖需要互斥鎖和條件兩項來實現,雖然看起來沒什麼關係,但在執行時中,互斥鎖將會與條件結合起來。執行緒將被一個互斥和條件結合的訊號來喚醒。

首先初始化條件和互斥鎖,當 ready_to_goflase 的時候,進入迴圈,然後執行緒將會被掛起,直到另一個執行緒將 ready_to_go 設定為 true 的時候,並且傳送訊號的時候,該執行緒會才被喚醒。

OSSpinLock

自旋鎖,和互斥鎖類似,都是為了保證執行緒安全的鎖。但二者的區別是不一樣的,對於互斥鎖,當一個執行緒獲得這個鎖之後,其他想要獲得此鎖的執行緒將會被阻塞,直到該鎖被釋放。但自選鎖不一樣,當一個執行緒獲得鎖之後,其他執行緒將會一直迴圈在哪裡檢視是否該鎖被釋放。所以,此鎖比較適用於鎖的持有者儲存時間較短的情況下。

然而,YYKit 作者 @ibireme 的文章也有說這個自旋鎖存在優先順序反轉問題,具體文章可以戳 不再安全的 OSSpinLock

os_unfair_lock

自旋鎖已經不在安全,然後蘋果又整出來個 os_unfair_lock_t (╯‵□′)╯︵┻━┻
這個鎖解決了優先順序反轉問題。

dispatch_semaphore

訊號量機制實現鎖,等待訊號,和傳送訊號,正如前邊所說的看門人一樣,當有多個執行緒進行訪問的時候,只要有一個獲得了訊號,其他執行緒的就必須等待該訊號釋放。

@synchronized

一個便捷的建立互斥鎖的方式,它做了其他互斥鎖所做的所有的事情。

如果你在不同的執行緒中傳過去的是一樣的識別符號,先獲得鎖的會鎖定程式碼塊,另一個執行緒將被阻塞,如果傳遞的是不同的識別符號,則不會造成執行緒阻塞。

總結

應當針對不同的操作使用不同的鎖,而不能一概而論那種鎖的加鎖解鎖速度快。

  • 當進行檔案讀寫的時候,使用 pthread_rwlock 較好,檔案讀寫通常會消耗大量資源,而使用互斥鎖同時讀檔案的時候會阻塞其他讀檔案執行緒,而 pthread_rwlock 不會。
  • 當效能要求較高時候,可以使用 pthread_mutex 或者 dispath_semaphore,由於 OSSpinLock 不能很好的保證執行緒安全,而在只有在 iOS10 中才有 os_unfair_lock ,所以,前兩個是比較好的選擇。既可以保證速度,又可以保證執行緒安全。
  • 對於 NSLock 及其子類,速度來說 NSLock NSCondition NSRecursiveLock NSConditionLock 。

參考

  1. Threading Programming Guide
  2. 百度百科-執行緒安全
  3. 百度百科-訊號量
  4. 百度百科-互斥鎖
  5. 不再安全的 OSSpinLock

相關文章