【Swift】iOS 線程鎖

Swift 中var生命的變量默認是非原子性的,若是要保證線程安全,咱們就須要引入鎖的感念。html

注意:謹慎直接在Demo中用for+print()等來證實是否線程安全。由於print()方法自己是線程安全的,它可能會拯救你的不安全代碼。第3節objc_sync部分的例子有print()和NSLog()的比較,結果僅做參考。編程

  1. 本文將着重介紹NSCondition以及DispatchSemaphore
  2. 本文介紹的內容Demo代碼都是基於Swift4.0

一 互斥鎖

iOS裏的線程互斥鎖主要有如下幾種安全

  1. 遵循NSLocking協議. 包括NSLockNSConditionNSConditionLockNSRecursiveLock
  2. GCD. DispatchSemaphore, DispatchWorkItemFlags.barrier
  3. objc_sync. 包括 @synchronized
  4. pthread. 包括POSIX。POSIX比較底層,可是通常不多用了。在此對POSIX也不詳述。文末有相關討論的資源。

1. NSLocking協議

NSlocking協議自己僅僅定義了lock()和unlock()bash

public protocol NSLocking {

    
    public func lock() 

    public func unlock()
}
複製代碼

除了NSCondition,其它三種鎖都是有如下兩個方法多線程

  1. open func `try`() -> Bool
    嘗試鎖,若是成功,返回true。這裏須要注意的是,若是對一個已經調用lock()加鎖的程序再次加鎖會產生死鎖,此時不會有返回值。(參考下面注意點4.1)併發

  2. open func lock(before limit: Date) -> Bool
    加鎖,而且給這個鎖一個過時時間,時間到了自動解鎖。app

1.1 NSLock

open class NSLock : NSObject, NSLocking {

    
    open func `try`() -> Bool 

    open func lock(before limit: Date) -> Bool

    
    @available(iOS 2.0, *)
    open var name: String?
}

複製代碼

最經常使用的鎖。在須要加鎖的地方lock(),而後在解鎖的地方unlock()便可。異步

1.2 NSCondition

@available(iOS 2.0, *)

open class NSCondition : NSObject, NSLocking {

    /// 阻塞(休眠)線程。收到信號喚醒
    open func wait()
    /// 休眠當前線程,並設置一個自動喚醒的時間
    open func wait(until limit: Date) -> Bool 
    /// 發出信號 喚醒一個線程
    open func signal() 
     /// 發出信號,所用運用當前NSConditionh實例的wait()線程都會喚醒
    open func broadcast()

    
    @available(iOS 2.0, *)
    open var name: String?
}
複製代碼

下面詳細介紹一下NSConditionasync

1.2.1. 執行步驟僞代碼

lock the condition // 鎖住condition
while (!(boolean_predicate)) { // 一個做爲判斷用的Bool值
    wait on condition // Wait()
}
do protected work // 執行任務代碼
(optionally, signal or broadcast the condition again or change a predicate value) // 經過signal或者baroadcast來改變狀態
unlock the condition // 解鎖condition
複製代碼

1.2.2. 簡介

wait()其實並不能直接用於鎖住線程,做用原理以下。
調用condition wait()以後,condition 實例會解鎖它已有的鎖(保證同時只有一個鎖)而後使當前調用的線程休眠。當 condition 被signal()通知後,系統會喚起線程。而後 condition 實例會在wait()或者wait(until:)方法結束的位置再次給線程加鎖。所以,從線程的角度來看,就好像wait()一直在保有這個鎖。
雖然wait()會給線程加鎖,在測試的時候也確實能夠按照指望運行,可是根據蘋果官方文檔, 單隻使用wait()加鎖並不能確保安全。因此,不管什麼狀況使用Condition的時候,第一步老是加鎖。鎖住當前condition能夠確保判斷和任務代碼不會受其它使用相同condition的線程影響。函數

基於condition發信號的原理,使用Bool值來判斷是很是重要的。給condition發射信號並不能保證condition自己爲true。因爲發信號的時間問題可能會致使假信號的出現。使用Bool值來判斷能夠確保以前的信號不會形成代碼在還不能安全運行的時候執行。這個判斷值就是一個很簡單Bool標籤,僅僅是用來判斷信號是否發射完成。 這部分的內容其實和用 POSIX Thread Locks中的情形同樣。 wait()函數的內部僞代碼以下

unlock the thread
wait for the signal
lock the thread
複製代碼

在使用的時候應當以下(不包含Bool判斷)

self.lock.lock()
self.lock.wait()
self.lock.unlock()
複製代碼

1.3 NSConditionLock

使用NSConditionLock,能夠確保線程僅在condition符合狀況時上鎖,而且執行相應的代碼,而後分配新的狀態。狀態值須要本身定義。

1.4 NSRecurisiveLock

NSRecursiveLock 定義了一種能夠屢次給相同線程上鎖並不會形成死鎖的鎖。

2. GCD

GCD裏的DispatchSemaphore,和DispatchWorkItemFlagBarrier也是能夠達到線程鎖的目的

2.1 DispatchSemaphore

open class DispatchSemaphore : DispatchObject {
}
extension DispatchSemaphore {

    public func signal() -> Int // 信號量增長1

    public func wait() // 信號量減小1

    public func wait(timeout: DispatchTime) -> DispatchTimeoutResult // 信號量減小1 並設置在timeout時間後加回這個減小的信號量1

    public func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult
}
extension DispatchSemaphore {
    @available(iOS 4.0, *)
    public /*not inherited*/ init(value: Int)
}
複製代碼

2.1.1 初始化

DispatchSemaphore(value: value)
value對應着最大信號值,因此信號值能夠對應到以下應用場景

  1. 當初始化的value值等於0,適用於兩個線程之間協調任務。
  2. 當初始化的value值小於0,會形成返回Null,初始化失敗。
  3. 當初始化的value值大於0,適合於管理一個有限的資源池,資源池的大小等於value值。
    而對於某個使用DispatchSemaphore來加鎖的線程來講,僅噹噹前信號量小於或等於初始值時纔會執行。

2.1.2 Demo

class GCDLockTest: TestBase {
    let semaphore = DispatchSemaphore(value: 1) // 此value值是最大信號值
    func test() {
        
        queueA.async {
            //直到經過signal()增長信號量
            self.semaphore.wait()  // 信號值 +1
            print("QueueA Gonna Sleep")
            sleep(3)
            print("QueueA Woke up")
            self.semaphore.signal()  // 信號值 -1
            
        }
        queueB.async {
            self.semaphore.wait()  // 信號值 +1
            print("QueueB Gonna Sleep")
            sleep(3)
            print("QueueB Work up")
            self.semaphore.signal()  // 信號值 -1

        }
        queueC.async {
            self.semaphore.wait()  // 信號值 +1
            print("QueueC Gonna Sleep")
            sleep(3)
            print("QueueC Wake up")
            self.semaphore.signal()  // 信號值 -1
            
        }
    }
}  
複製代碼

若是初始化value是1 那麼輸出將會是

QueueA Gonna Sleepd // 信號量+1 (當前任務正在進行,能夠理解爲佔用資源池1個資源)
// 3秒 期間queueB, queueC 並無執行,由於信號量初始化的值1,也就是最大容許1,能夠理解爲資源池只有一個資源
QueueA Woke up // QeueuA 完成 信號量-1  當前信號量0,小於1.因而下一個線程開始執行
QueueB Gonna Sleep // QueueB執行 信號量+1
// 3秒
QueueB Work up // QueueB結束 信號量-1
QueueC Gonna Sleep // QueueC執行 信號量+1
// 3秒
QueueC Wake up // QueueC 結束 信號量+1

複製代碼

同理,若是初始化值爲2,最大能夠同時兩個線程執行。
若是初始值是3的話咱們的Demo中三個線程就均可以同時執行。
那若是初始化0呢?
顯然,本例中的三個線程都將不能執行,由於信號量一直高於初始值。如今回看咱們在2.1中提到的應用場景,是否是就很好理解。

咱們將QueueAQueueB稍做改變

queueA.async {
            //直到經過signal()增長信號量
            self.semaphore.wait()  // 信號值 +1
            print("QueueA Gonna Sleep")
            sleep(3)
            print("QueueA Woke up")
            self.semaphore.signal()  // 信號值 -1
            
        }
        queueB.async {
            self.semaphore.signal()  // 信號值 +1
            print("QueueB Gonna Sleep")
            sleep(3)
            print("QueueB Work up")
        }
  
複製代碼

這時的輸出會是什麼?

QueueB Gonna Sleep // 由於QueueA在執行的時候信號值+1,超過了0,因此只能等待
QueueA Gonna Sleep // 當QueueB執行的時候,信號值-1,沒有超過0,因此QueueA就能執行了
/// 3秒
QueueB Work up
QueueA Woke up
複製代碼

因此,當初始化值爲0時,就能夠達到兩個線程其中一個再另外一個以後結束等功能。

注意: 若是在主線程中wait()會阻塞UI刷新

2.3 DispatchGroup

enter()是明確告訴GCD你要開始
leave()是明確標明任務結束
通常狀況下不須要明確使用enter()/leave() 。 只有好比說,你的任務中包含其它異步任務,而你想要在這個子異步任務開始前就結束等待,那就可使用leave()了。

3. objc_sync

3.1 objc_sync_enter/objc_sync_exit

class SyncTest {
    var count = 0
    func test() {
        count = 0
        
        let queueA = DispatchQueue(label: "Q1")
        let queueB = DispatchQueue(label: "Q2")
        let queueC = DispatchQueue(label: "Q3")
        
        queueA.async {
            for _ in 1...100 {
                NSLog("%i", self.increased())
//                print(self.increased())
            }
        }
        queueB.async {
            for _ in 1...100 {
                NSLog("%i", self.increased())
//                print(self.increased())
            }
        }
        queueC.async {
            for _ in 1...100 {
                NSLog("%i", self.increased())
//                print(self.increased())
            }
            
        }
      
        
    }
    func increased() -> Int {
        objc_sync_enter(count)
        count += 1
        objc_sync_exit(count)
        return count
    }
}
複製代碼

3.1.1

objc_sync_enter(object)方法會在object上開啓同步(synchronize),若是成功返回OBJC_SYNC_SUCCESS, 不然返回OBJC_SYNC_NOT_OWNING_THREAD_ERROR ,直到objc_sync_exit(object)

objectAny類型。在本Demo中甚至能夠直接傳入self。可是它會鎖住整個

二 自旋鎖

主要介紹兩種

  1. OSSpinLock。因爲存在由於低優先級爭奪資源致使的死鎖,在iOS10.0以後已廢棄,並引入下面的新方法。
  2. os_unfair_lock。替代OSSpinLock的自旋鎖方案。須要導入os

三 性能比較

引用一張被普遍引用在此類文章中的圖片來講明

根據我後來本身作的測試,OSSpinLock和os_unfair_lock以及dispatch_semaphore三者的性能是最優且接近的。

四 注意點

1. 串行隊列即便異步執行也不會從新開新線程。參考第二點後面的例子。
2. 主線程隊列是單一線程串行隊列的。不要在主線程加鎖,會致使UI刷新被阻塞。

for i in 1...10 {
            DispatchQueue.global().async {
                print("\(i)---\(Thread.current)")
            }
        }

      
複製代碼
for i in 1...10 {
            DispatchQueue.main.async {
                print("\(i)---\(Thread.current)")
            }
        }

      
複製代碼

輸出

5---<NSThread: 0x60c000074280>{number = 11, name = (null)}
2---<NSThread: 0x60800006e500>{number = 5, name = (null)}
6---<NSThread: 0x60400007a0c0>{number = 6, name = (null)}
3---<NSThread: 0x60400007a140>{number = 10, name = (null)}
9---<NSThread: 0x60800006e6c0>{number = 12, name = (null)}
4---<NSThread: 0x600000069b40>{number = 4, name = (null)}
8---<NSThread: 0x60400007a080>{number = 3, name = (null)}
1---<NSThread: 0x60800006e600>{number = 9, name = (null)}
7---<NSThread: 0x60400007a100>{number = 8, name = (null)}
10---<NSThread: 0x60000046c6c0>{number = 7, name = (null)}

複製代碼
1---<NSThread: 0x60c000077d40>{number = 1, name = main}
2---<NSThread: 0x60c000077d40>{number = 1, name = main}
3---<NSThread: 0x60c000077d40>{number = 1, name = main}
4---<NSThread: 0x60c000077d40>{number = 1, name = main}
5---<NSThread: 0x60c000077d40>{number = 1, name = main}
6---<NSThread: 0x60c000077d40>{number = 1, name = main}
7---<NSThread: 0x60c000077d40>{number = 1, name = main}
8---<NSThread: 0x60c000077d40>{number = 1, name = main}
9---<NSThread: 0x60c000077d40>{number = 1, name = main}
10---<NSThread: 0x60c000077d40>{number = 1, name = main}

複製代碼

3. 併發和並行: 並行是線程被多個CPU內核執行,併發是線程輪流交替被單個CPU內核執行。
4. 上鎖的英文 acquire a lock
5. 對一個已經lock()的鎖再次調用lock()將會產生死鎖,這也是遞歸鎖引入的緣由。遞歸鎖實現的就是能夠屢次加鎖也不會產生死鎖。

BTW: 一樣的死鎖會產生在在同一個同步線程中調用這個線程的同步隊列

五 資源

本例代碼後續會上傳到Github
蘋果官方多線程編程指南
POSIX博客

相關文章
相關標籤/搜索