廣義多執行緒安全之鎖與iOS鎖簡單介紹

灰s發表於2017-12-22

原子的(Atomic)

工程師所寫的任意一條程式碼,被編譯成彙編程式碼之後可能不止一條指令(例如++),因此在執行的時候可能執行一半就被排程系統打斷,去執行別的程式碼。這也是多執行緒會不安全的根本原因。

而我們把單指令的操作稱為原子的,因為無論如何,單條指令的執行是不會被打斷的。很多體系結構都提供了一些常用操作的原子指令。

廣義的同步與鎖

為了避免多個執行緒同時讀取一個資料而產生不可預料的後果,我們要將各個執行緒對同一個資料的訪問同步(Synchronization)。所謂同步,即指在一個執行緒訪問資料未結束的時候,其他的執行緒不得對同一個資料進行訪問。如此,對資料的訪問被原子化了。

同步的最常見方法是使用鎖(Lock)。鎖是一種非強制機制,每一個執行緒在訪問資料或資源之前首先試圖 獲取(Acquire) 鎖,並在訪問結束之後 釋放(Release) 鎖。在鎖已經被佔用的時候試圖獲取鎖時,執行緒會等待,直到鎖重新可用。

1. 二元訊號量(Binary Semaphore)

最簡單的一種鎖,它只有兩種狀態:佔用與非佔用。它適合只能被唯一一個執行緒獨佔訪問的資源。當二院訊號量處於非佔用狀態時,第一個試圖獲取該二元訊號量的執行緒會獲得該鎖,並將二元訊號量置為佔用狀態,此後其他的所有試圖獲取該二元訊號量的執行緒將會等待,直到該鎖被釋放。

這裡的鎖釋放操作,可由其他執行緒執行。

2. 多元訊號量(簡稱: 訊號量Semaphore)

對於允許多個執行緒併發訪問的資源,它是一個很好的選擇。一個初始值為N的訊號量允許N個執行緒併發訪問。

執行緒訪問資源的時候首先獲取訊號量,進行如下操作:

  • 將訊號量的值減1
  • 如果訊號量的值小於0,則進入等待狀態,否則繼續執行

訪問完資源之後,執行緒釋放訊號量,進行如下操作:

  • 將訊號量的值加1
  • 如果訊號量的值小於1,喚醒一個等待中的執行緒

3. 互斥量(Mutex)

和二元訊號量很類似,資源僅同時允許一個執行緒訪問,但和訊號量不同的是,訊號量在整個系統可以被任意執行緒獲取並釋放,也就是說,同一個訊號量可以被系統中的一個執行緒獲取之後由另一個執行緒釋放

而互斥量則要求哪個執行緒獲取了互斥量,哪個執行緒就要負責釋放這個鎖,其他的執行緒越俎代庖去釋放互斥量是無效的。

4. 臨界區(Critical Section)

是比互斥量更加嚴格的同步手段。在屬於中,把臨界區的鎖的獲取稱為進入臨界區,而把鎖的釋放稱為離開臨界區。臨界區和互斥量與訊號量的區別在於,互斥量和訊號量在系統的任何程式裡都是可見的,也就是說,一個程式建立了一個互斥量或訊號量,另一個程式試圖去獲取該鎖是合法的。

然而,臨界區的作用範圍僅限於本程式,其他的程式無法獲取該鎖。除此之外,臨界區具有和互斥量相同的性質。

5. 讀寫鎖(Read-Write Lock)

對於一個讀寫鎖由兩種獲取方式,共享的(Shared)獨佔的(Exclusive)

當鎖處於自由的狀態時,試圖以任何一種方式獲取鎖都能成功,並將鎖置於對應的狀態。

如果鎖處於共享的狀態,其他執行緒以共享的方式獲取鎖仍然會成功,此時這個鎖分配給了多個執行緒。然而,如果其他的執行緒試圖以獨佔的方式獲取已經處於共享狀態的鎖,那麼它將必須等待鎖被所有的執行緒釋放。

如果鎖處於獨佔的狀態,將阻止任何其他執行緒獲取該鎖,不論它們試圖以那種方式獲取。

6. 條件變數(Condition Variable)

作為一個同步手段,作用類似與一個柵欄。對於條件變數,執行緒可以有兩種操作,首先執行緒可以等待條件變數,一個條件變數可以被多個執行緒等待。其次,執行緒可以喚醒條件變數,此時某個或所有等待此條件變數的執行緒都會被喚醒並繼續支援。也就是說,使用條件變數可以讓許多執行緒一起等待某個事件的發生,當事件發生時(條件變數被喚醒),所有的執行緒可以一起恢復執行。

iOS中的鎖

1. NSLock

NSLock實現了最基本的互斥鎖,遵循了NSLocking協議,通過lockunlock來進行鎖定和解鎖。其使用也非常簡單,由於是互斥鎖,當一個執行緒進行訪問的時候,該執行緒獲得鎖,其他執行緒進行訪問的時候,將被作業系統掛起,直到該執行緒釋放鎖,其他執行緒才能對其進行訪問,從而確保了執行緒安全。但是如果連續鎖定兩次,則會造成死鎖問題。

注意unLock操作必須執行在lock操作所執行的執行緒,不然可能會造成未知的錯誤

    _lock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"1");
        [self lockFounction:[NSThread currentThread] num: 1];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"2");
        [self lockFounction:[NSThread currentThread] num: 2];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"3");
        [self lockFounction:[NSThread currentThread] num: 3];
    });
複製程式碼
    - (void)lockFounction:(NSThread *)thread num:(NSInteger) num {
        [_lock lock];
        NSLog(@"thread - %@, num - %ld", thread, num);
        sleep(5);
        [_lock unlock];
    }
複製程式碼

以下為執行結果,正好每5秒執行一次。

image.png
然後這裡我想說一下tryLock方法,該方法的返回值描述YES if the lock was acquired, otherwise NO.直譯過來是獲取鎖成功返回YES,否則返回NO。然而它具體做的工作是嘗試獲取鎖,並在成功獲取的同時進行lock操作。未成功獲取時則什麼都不做。,那麼是否可以用它替代lock方法,從而避免多次進行鎖操作造成死鎖的情況呢?答案是否定的。

- (void)lockFounction:(NSThread *)thread num:(NSInteger) num {
    [_lock tryLock];
    NSLog(@"thread - %@ num - %ld", thread, num);
    sleep(5);
    [_lock unlock];
}
複製程式碼

比如把方法改成這樣,然而輸出結果卻不是之前那樣,如下圖,這裡根本就沒有起到上鎖的效果。在我的理解是,方法中只有在lockunlock中間的臨界區程式碼才能得到執行緒保護,但是你把lock改成tryLock以後,只有第一個進入該方法的執行緒,才會tryLock成功並等價的執行lock方法,但是對另外的執行緒根本就沒有約束力,因為他們獲取不到鎖,什麼都沒有執行。導致臨界區的方法直接被執行了,而不會被掛起。

image.png

2. NSRecursiveLock

大家都叫它遞迴鎖,它可以允許同一執行緒多次加鎖,而不會造成死鎖。遞迴鎖會跟蹤它被lock的次數。每次成功的lock都必須有對應的unlock操作。只有達到這種平衡,鎖最後才能被釋放,以供其它執行緒使用。下面直接上程式碼和執行效果。它的tryLock方法,和NSLock是相同的,都是嘗試獲取鎖的意思,這裡就不多做說明。

    _lock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"1");
        [self lockFounction:[NSThread currentThread] num: 1 count:5];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"2");
        [self lockFounction:[NSThread currentThread] num: 2 count:6];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"3");
        [self lockFounction:[NSThread currentThread] num: 3 count:7];
    });
複製程式碼
    - (void)lockFounction:(NSThread *)thread num:(NSInteger) num count:(NSInteger)count {
        [_lock lock];
        NSLog(@"thread - %@ num - %ld, count - %ld", thread, num, count);
        if (count != 0) {
            count --;
            sleep(2);
            [self lockFounction:thread num:num count:count];
        }
        [_lock unlock];
    }
複製程式碼

image.png

3. NSCondition

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

    _ifFinished = NO;
    _lock = [[NSCondition alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self downLoad];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self doSomeThing];
    });
複製程式碼
- (void)downLoad {
    NSLog(@"downLoad begin %@", [NSThread currentThread]);
    [_lock lock];
    sleep(5);
    NSLog(@"downLoad finish");
    _ifFinished = YES;
    [_lock signal];    //傳送訊號
    [_lock unlock];
}

- (void)doSomeThing {
    [_lock lock];
    NSLog(@"doSomeThing begin %@", [NSThread currentThread]);
    while (!_ifFinished) {
        NSLog(@"wait");
        [_lock wait];        //等待訊號
    }
    NSLog(@"doSomeThing end %@", [NSThread currentThread]);
    [_lock unlock];
}
複製程式碼

image.png
注意事項:

  • wait方法的本質是鎖的轉移,消費者放棄鎖,然後生產者獲得鎖,同理,signal則是一個鎖從生產者到消費者轉移的過程。
  • 這裡一個signal訊號,只能對應一個wait,如果存在多個wait,只會喚醒第一個。
  • NSConditionlock也有互斥效果,但是在執行wait方法放棄鎖,執行緒進行等待的時候,鎖的效果將會失效。
  • 自然我們會有疑問:“如果不用互斥鎖,只用條件變數(waitsignal)會有什麼問題呢?”。downLoad方法睡眠的那5秒(即模擬獲取資料的過程); 這段程式碼不是執行緒安全的,也許在你把資料獲取出來以前,已經有別的執行緒修改了資料。因此我們需要保證消費者拿到的資料是執行緒安全的。
  • wait方法除了會被signal方法喚醒,有時還會被虛假喚醒,所以需要這裡while迴圈中的判斷來做二次確認。

4. NSConditionLock

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

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

    //condition預設是0
    _lock = [[NSConditionLock alloc] initWithCondition:5];
    NSTimer * timer1 = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self producer];
        });
    }];
    NSTimer * timer2 = [NSTimer timerWithTimeInterval:3 repeats:YES block:^(NSTimer * _Nonnull timer) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self consumer];
        });
    }];
    [[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSRunLoopCommonModes];
    [[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSRunLoopCommonModes];
複製程式碼
- (void)producer {
    /**
     這裡如果直接使用 lockWhenCondition: 方法。
     跑幾分鐘後就會卡死,猜測是因為鎖死。具體的原因我沒有想明白,如果你想明白了麻煩告訴我。
     
     用tryLock方法,我跑了10幾分鐘依舊正常執行。
     並且注意,只有在tryLock成功的情況下才進行具體的操作。
     */
    if ([_lock tryLockWhenCondition:5]) {
        NSLog(@"have something %@", [NSThread currentThread]);
        _count ++;
        [_lock unlockWithCondition:6];
    }
}

- (void)consumer {
    if ([_lock tryLockWhenCondition:6]) {
        NSLog(@"use something %@", [NSThread currentThread]);
        _count --;
        NSLog(@"%ld", _count);
        [_lock unlockWithCondition:5];
    }
}
複製程式碼

image.png

參考連結:

iOS 中的各種鎖

深入理解 iOS 開發中的鎖

相關文章