這是併發控制方案的系列文章,介紹了各種鎖的使用及優缺點。
OSSpinLock、os_unfair_lock、pthread_mutex_t、pthread_cond_t、pthread_rwlock_t 是值類型,不是引用類型。這意味着使用 = 會進行復制,使用複製的可能導致閃退。pthread 函數認爲其一直處於初始化的內存地址,將其移動到其他內存地址會產生問題。使用copy的OSSpinLock不會崩潰,但會得到一個全新的鎖。
如果你對線程、進程、串行、併發、並行、鎖等概念還不瞭解,建議先查看以下文章:
自旋鎖(Spin Lock)是一種簡單、高效、線程安全的同步原語(synchronization primitive),其在等待時會反覆檢查鎖的狀態,直到解鎖。
鎖已經加鎖時,多數鎖會讓嘗試加鎖的線程進入睡眠狀態,釋放鎖時再將其喚醒。這在多數情況下都是合適的,但如果臨界區域特別小,耗時極短,常規鎖的休眠、喚醒操作將變得昂貴。此時,自旋鎖的忙等性能更高。
Spin lock 使用 memory barrier 保護共享資源,鎖定期間可能發生搶佔(preemption)。
1. 多線程同時訪問同一資源
爲了方便本系列文章介紹其他鎖,先創建一個需要線程同步的基類,每次介紹鎖時只需繼承自該基類即可。
class BaseDemo {
private var ticketsCount = 25
private var money = 100
// MARK: - Money
func moneyTest() {
let queue = DispatchQueue.global(qos: .utility)
queue.async {
for _ in 1...10 {
self.saveMoney()
}
}
queue.async {
for _ in 1...10 {
self.drawMoney()
}
}
}
func drawMoney() {
var oldMoney = money
sleep(1)
oldMoney -= 20
money = oldMoney
print("取20元,還剩餘\(oldMoney)元 -- \(Thread.current)")
}
func saveMoney() {
var oldMoney = money
sleep(1)
oldMoney += 50
money = oldMoney
print("存50元,還剩\(oldMoney)元 -- \(Thread.current)")
}
// MARK: - Sale Ticket
func ticketTest() {
let queue = DispatchQueue.global(qos: .utility)
queue.async {
for _ in 1...5 {
self.saleTicket()
}
}
queue.async {
for _ in 1...5 {
self.saleTicket()
}
}
queue.async {
for _ in 1...5 {
self.saleTicket()
}
}
}
func saleTicket() {
var oldTicketsCount = ticketsCount
sleep(1)
oldTicketsCount -= 1
ticketsCount = oldTicketsCount
print("還剩\(oldTicketsCount)張票 -- \(Thread.current)")
}
func otherTest() {
}
}
即使執行
i = i +1
這樣簡單的命令,也可分爲三個指令:
- 讀取 i 的值。
- 對 i 的值進行加一。
- 將值寫入 i。
執行上述任一指令時都可能發生上下文切換,也可能多個線程同時操作。如,線程A讀取 i 的值,線程B同時讀取 i 的值,進行加一後,線程A寫入後,線程B也進行寫入。這會導致 i 的值只進行了一次加一操作。想要解決這一問題,應採取線程同步措施。
調用moneyTest()
、ticketTest()
函數時觸發併發存取款、賣票,會產生不可預期的結果。後續部分只需要在調用基類方法時加鎖、解鎖即可。
2. 自旋鎖 API
2.1 初始化OSSpinLock
OSSpinLock
是數值類型,未鎖定時值爲零,鎖定時值爲非零。使用以下代碼創建OSSpinLock
屬性:
private var moneyLock: OSSpinLock = OS_SPINLOCK_INIT
private var ticketLock: OSSpinLock = OS_SPINLOCK_INIT
如果是在 Objective-C 中使用自旋鎖,需導入
#import <libkern/OSAtomic.h>
頭文件。
2.2 加鎖OSSpinLockLock() OSSpinLockTry()
加鎖時調用OSSpinLockLock()
、OSSpinLockTry()
。如果鎖已經加鎖,OSSpinLockLock()
函數會忙等(busy waiting),其也會採取一些策略避免優先級反轉,但對於執行時間長、競爭激烈的任務效率不高。如果已經加鎖,OSSpinLockTry()
立即返回 false,不會忙等。
加鎖方法如下:
os_unfair_lock_lock(&moneyLock)
2.3 解鎖OSSpinLockUnlock()
解鎖時調用OSSpinLockUnlock()
函數。
os_unfair_lock_unlock(&moneyLock)
更新後,OSSpinLockDemo.swift
文件如下:
class OSSpinLockDemo: BaseDemo {
private var moneyLock: OSSpinLock = OS_SPINLOCK_INIT
private var ticketLock: OSSpinLock = OS_SPINLOCK_INIT
override func drawMoney() {
OSSpinLockLock(&moneyLock)
super.drawMoney()
OSSpinLockUnlock(&moneyLock)
}
override func saveMoney() {
OSSpinLockLock(&moneyLock)
super.saveMoney()
OSSpinLockUnlock(&moneyLock)
}
override func saleTicket() {
OSSpinLockLock(&ticketLock)
super.saleTicket()
OSSpinLockUnlock(&ticketLock)
}
}
未加鎖時,執行結果可能出現錯誤;加鎖後多次執行,結果均爲正確。
3. 自旋鎖性能
當條件合適時,自旋鎖性能最佳。自旋鎖的問題在於,當一個線程持有鎖時,其他嘗試加鎖的線程會浪費 CPU 資源忙等。
如果臨界區域很小,一般不會出現問題。如果一個線程加鎖後,沒有其他線程嘗試獲取鎖,也不會出現問題。如果其他線程也嘗試獲取鎖,其必須等待持有鎖的線程執行完畢。在單核的設備上,這一問題更爲突出。因爲持有鎖的線程必須等待自旋的線程使用完分配的時間片才能執行。
也可以採取一些措施減少此類問題。例如,對自旋次數計數,達到一定次數後,將資源讓步於調度程序。OSSpinLock
實現了一些類似策略,在大多數情況下,OSSpinLock
運行良好,甚至可以避免優先級反轉。
4. 優先級反轉
優先級反轉指低優先級線程持有鎖,高優先級線程被鎖阻塞、或等待低優先級線程執行結果。在常規鎖中,只是高優先級線程需等待低優先級線程執行,由於低優先級線程被分配資源少,可能需要等待很長時間。但在 spin lock 中,這一問題更爲嚴峻。因爲等待鎖的高優先級線程等待時一直自旋,佔用 CPU 資源,低優先級線程分配到的資源更少,進一步導致鎖長時間不能釋放。
OSSpinLock
會採取一些策略緩和優先級反轉的問題。例如,自旋一定次數後,如果加鎖線程進度沒有變化,停止自旋。dispatch queue 和 pthread mutex 通過自動提高持有鎖線程的優先級解決優先級反轉問題。由於信號量(如,dispatch_semaphore_t)不知道哪個線程正在執行工作,其不會進行類似處理。
iOS 8 內核升級後推出了 Quality Of Service(簡稱 QOS)。QOS 允許NSOperation、NSThread
、dispatch queue 和 pthread 將任務分爲不同優先級。擁有高 QOS 的線程永遠不會衰減爲低 QOS,調度器永遠會優先爲高 QOS 的線程分配資源。因此,處於自旋的 QOS 線程會持續忙等,持有鎖的低 QOS 的線程得不到資源執行任務,導致自旋鎖不再安全。
爲此,iOS 10 使用os_unfair_lock取代了OSSpinLock
。下一篇文章將介紹os_unfair_lock
。
Demo名稱:Synchronization
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/Synchronization
參考資料: