執行緒安全: 互斥鎖和自旋鎖(10種)

Dariel發表於2018-11-19

執行緒安全: 互斥鎖和自旋鎖(10種)

無併發,不程式設計.提到多執行緒就很難繞開鎖?.

iOS開發中較常見的兩類鎖:

1. 互斥鎖: 同一時刻只能有一個執行緒獲得互斥鎖,其餘執行緒處於掛起狀態.
2. 自旋鎖: 當某個執行緒獲得自旋鎖後,別的執行緒會一直做迴圈,嘗試加鎖,當超過了限定的次數仍然沒有成功獲得鎖時,執行緒也會被掛起.

自旋鎖較適用於鎖的持有者儲存時間較短的情況下,實際使用中互斥鎖會用的多一些.

1. 互斥鎖,訊號量

1.遵守NSLocking協議的四種鎖

四種鎖分別是:
NSLockNSConditionLockNSRecursiveLockNSCondition

NSLocking協議

public protocol NSLocking {    
    public func lock()
    public func unlock()
}
複製程式碼

下面舉個多個售票點同時賣票的例子

var ticket = 20
var lock = NSLock()
    
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    let thread1 = Thread(target: self, selector: #selector(saleTickets), object: nil)
    thread1.name = "售票點A"
    thread1.start()
    
    let thread2 = Thread(target: self, selector: #selector(saleTickets), object: nil)
    thread2.name = "售票點B"
    thread2.start()
}
	
@objc private func saleTickets() {
	while true {
	    lock.lock()
	    Thread.sleep(forTimeInterval: 0.5) // 模擬延遲
	    if ticket > 0 {
	        ticket = ticket - 1
	        print("\(String(describing: Thread.current.name!)) 賣出了一張票,當前還剩\(ticket)張票")
	        lock.unlock()
	    }else {
	        print("oh 票已經賣完了")
	        lock.unlock()
	        break;
	    }
	}
}
複製程式碼

遵守協議後實現的兩個方法lock()unlock(),意如其名.

除此之外NSLockNSConditionLockNSRecursiveLockNSCondition四種互斥鎖各有其實現:

1. 除NSCondition外,三種鎖都有的兩個方法:
    // 嘗試去鎖,如果成功,返回true,否則返回false
    open func `try`() -> Bool
    // 在limit時間之前獲得鎖,沒有返回NO
    open func lock(before limit: Date) -> Bool
複製程式碼
2. NSCondition條件鎖:
    // 當前執行緒掛起
    open func wait()
    // 當前執行緒掛起,設定一個喚醒時間
    open func wait(until limit: Date) -> Bool
    // 喚醒在等待的執行緒
    open func signal()
    // 喚醒所有NSCondition掛起的執行緒
    open func broadcast()
複製程式碼

當呼叫wait()之後,NSCondition例項會解鎖已有鎖的當前執行緒,然後再使執行緒休眠,當被signal()通知後,執行緒被喚醒,然後再給當前執行緒加鎖,所以看起來好像wait()一直持有該鎖,但根據蘋果文件中說明,直接把wait()當執行緒鎖並不能保證執行緒安全.

3. NSConditionLock條件鎖:

NSConditionLock是藉助NSCondition來實現的,在NSCondition的基礎上加了限定條件,可自定義程度相對NSCondition會高些.

    // 鎖的時候還需要滿足condition
    open func lock(whenCondition condition: Int)
    // 同try,同樣需要滿足condition
    open func tryLock(whenCondition condition: Int) -> Bool
    // 同unlock,需要滿足condition
    open func unlock(withCondition condition: Int)
    // 同lock,需要滿足condition和在limit時間之前
    open func lock(whenCondition condition: Int, before limit: Date) -> Bool
複製程式碼
4. NSRecurisiveLock遞迴鎖:

定義了可以多次給相同執行緒上鎖並不會造成死鎖的鎖.

提供的幾個方法和NSLock類似.

2. GCD的DispatchSemaphore和柵欄函式
1. DispatchSemaphore訊號量:

DispatchSemaphore中的訊號量,可以解決資源搶佔的問題,支援訊號的通知和等待.每當傳送一個訊號通知,則訊號量+1;每當傳送一個等待訊號時訊號量-1,如果訊號量為0則訊號會處於等待狀態.直到訊號量大於0開始執行.所以我們一般將DispatchSemaphore的value設定為1.

下面給出了DispatchSemaphore的封裝類

class GCDSemaphore {
    // MARK: 變數
    fileprivate var dispatchSemaphore: DispatchSemaphore!
    // MARK: 初始化
    public init() {
        dispatchSemaphore = DispatchSemaphore(value: 0)
    }
    public init(withValue: Int) {
        dispatchSemaphore = DispatchSemaphore(value: withValue)
    }
    // 執行
    public func signal() -> Bool {
        return dispatchSemaphore.signal() != 0
    }
    public func wait() {
        _ = dispatchSemaphore.wait(timeout: DispatchTime.distantFuture)
    }
    public func wait(timeoutNanoseconds: DispatchTimeInterval) -> Bool {
        if dispatchSemaphore.wait(timeout: DispatchTime.now() + timeoutNanoseconds) == DispatchTimeoutResult.success {
            return true
        } else {
            return false
        }
    }
}
複製程式碼
2. barrier柵欄函式:

柵欄函式也可以做執行緒同步,當然了這個肯定是要並行佇列中才能起作用.只有噹噹前的並行佇列執行完畢,才會執行柵欄佇列.

/// 建立併發佇列
let queue = DispatchQueue(label: "queuename", attributes: .concurrent)
/// 非同步函式
queue.async {
    for _ in 1...5 {
        print(Thread.current)
    }
}
queue.async {
    for _ in 1...5 {
        print(Thread.current)
    }
}
/// 柵欄函式
queue.async(flags: .barrier) {
    print("barrier")
}
queue.async {
    for _ in 1...5 {
        print(Thread.current)
    }
}
複製程式碼
3. 其他的互斥鎖
1. pthread_mutex互斥鎖

pthread表示POSIX thread,跨平臺的執行緒相關的API,pthread_mutex也是一種互斥鎖,互斥鎖的實現原理與訊號量非常相似,阻塞執行緒並睡眠,需要進行上下文切換.

一般情況下,一個執行緒只能申請一次鎖,也只能在獲得鎖的情況下才能釋放鎖,多次申請鎖或釋放未獲得的鎖都會導致崩潰.假設在已經獲得鎖的情況下再次申請鎖,執行緒會因為等待鎖的釋放而進入睡眠狀態,因此就不可能再釋放鎖,從而導致死鎖.

這邊給出了一個基於pthread_mutex_t(安全的"FIFO"互斥鎖)的封裝 MutexLock

1. @synchronized條件鎖

日常開發中最常用的應該是@synchronized,這個關鍵字可以用來修飾一個變數,併為其自動加上和解除互斥鎖.這樣,可以保證變數在作用範圍內不會被其他執行緒改變.但是在swift中它已經不存在了.其實@synchronized在幕後做的事情是呼叫了objc_sync中的objc_sync_enterobjc_sync_exit 方法,並且加入了一些異常判斷.

因此我們可以利用閉包自己封裝一套.

func synchronized(lock: AnyObject, closure: () -> ()) {
    objc_sync_enter(lock)
    closure()
    objc_sync_exit(lock)
}

// 使用
synchronized(lock: AnyObject) {
    // 此處AnyObject不會被其他執行緒改變
}

複製程式碼

2. 自旋鎖

1. OSSpinLock自旋鎖

OSSpinLock是執行效率最高的鎖,不過在iOS10.0以後已經被廢棄了.

詳見大神ibireme的不再安全的 OSSpinLock

2. os_unfair_lock自旋鎖

它能夠保證不同優先順序的執行緒申請鎖的時候不會發生優先順序反轉問題.這是蘋果為了取代OSSPinLock新出的一個能夠避免優先順序帶來的死鎖問題的一個鎖,OSSPinLock就是有由於優先順序造成死鎖的問題.

注意: 這個鎖適用於小場景下的一個高效鎖,否則會大量消耗cpu資源.

var unsafeMutex = os_unfair_lock()
os_unfair_lock_lock(&unsafeMutex)
os_unfair_lock_trylock(&unsafeMutex)
os_unfair_lock_unlock(&unsafeMutex)
複製程式碼

這邊給出了基於os_unfair_lock的封裝 MutexLock

3. 效能比較

這邊貼一張大神ibireme在iPhone6、iOS9對各種鎖的效能測試圖

執行緒安全: 互斥鎖和自旋鎖(10種)

本文收錄於 SwiftTips

參考:
不再安全的OSSpinLock
深入理解iOS開發中的鎖

如有疑問,歡迎留言 :-D

相關文章