iOS開發基礎——執行緒安全(執行緒鎖)

joker_jm發表於2019-02-19

小記

在IOS上進行多執行緒開發,為了保證執行緒安全,防止資源競爭,需要給程式進行加鎖,通常用到的程式鎖分為7種。

  • 訊號量
  • 互斥鎖
  • 自旋鎖
  • 遞迴鎖
  • 條件鎖
  • 讀寫鎖
  • 分散式鎖

鎖:是保證執行緒安全常見的同步工具,防止Data race(資料競爭)的發生。

Data race(資料競爭):

  • 兩個或者更多執行緒在一個程式中,併發的訪問同一資料
  • 至少一個訪問是寫入操作
  • 些執行緒都不使用任何互斥鎖來控制這些訪問

pthread_mutex

pthread_mutexattr_t attr;  
pthread_mutexattr_init(&attr);  
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定義鎖的屬性

pthread_mutex_t mutex;  
pthread_mutex_init(&mutex, &attr) // 建立鎖
pthread_mutex_lock(&mutex); // 申請鎖  
pthread_mutex_unlock(&mutex); // 釋放鎖  
複製程式碼

其中,鎖的屬性包含一下四種:

PTHREAD_MUTEX_NORMAL:預設值普通鎖,當一個執行緒加鎖以後,其他執行緒進入按照優先順序進入等待佇列,並且解鎖的時候按照先入先出的方式獲得鎖。
PTHREAD_MUTEX_ERRORCHECK:檢錯鎖,當同一個執行緒獲得同一個鎖的時候,則返回EDEADLK,否則與普通鎖處理一樣。
PTHREAD_MUTEX_RECURSIVE:遞迴鎖。這裡有別於上面的檢錯鎖,同一個執行緒可以遞迴獲得鎖,但是加鎖和解鎖必須要一一對應。
PTHREAD_MUTEX_DEFAULT:適應鎖,等待解鎖之後重新競爭,沒有等待佇列。
複製程式碼

訊號量

dispatch_semaphore是GCD用來同步的一種方式,dispatch_semephore_create方法使用者建立一個dispatch_semephore_t型別的訊號量,初始的引數必須大於0,該引數用來表示該訊號量有多少個訊號,簡單的說也就是同事允許多少個執行緒訪問。 dispatch_semaphore_wait方法是等待一個訊號量,該方法會判斷signal的訊號值是否大於0,如果大於0則不會阻塞執行緒,消耗點一個訊號值,執行後續任務。如果訊號值等於0那麼就和NSCondition一樣,阻塞當前執行緒進入等待狀態,如果等待時間未超過timeout並且dispatch_semaphore_signal釋放了了一個訊號值,那麼就會消耗掉一個訊號值並且向下執行。如果期間一直不能獲得訊號量並且超過超時時間,那麼就會自動執行後續語句。

  • dispatch_semaphore_create(long value);//創造訊號量
  • dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);//等待訊號
  • dispatch_semaphore_signal(dispatch_semaphore_t dsema);//傳送訊號

例項程式碼:

- (void)semaphoreSync {
    NSLog(@"semaphore---begin");
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //建立初始訊號量 為 0 ,阻塞所有執行緒
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任務A
        [NSThread sleepForTimeInterval:2];              // 模擬耗時操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 列印當前執行緒
        number = 100;
        // 執行完執行緒,訊號量加 1,訊號總量從 0 變為 1
        dispatch_semaphore_signal(semaphore);
    });
    //原任務B
    ////若計數為0則一直等待,直到接到總訊號量變為 >0 ,繼續執行後續程式碼
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %d",number);
}
複製程式碼

互斥鎖

互斥鎖的實現原理與訊號量非常相似,不是使用忙等,而是阻塞執行緒並睡眠,需要進行上下文切換。
當一個執行緒獲得這個鎖之後,其他想要獲得此鎖的執行緒將會被阻塞,直到該鎖被釋放。
當臨界區加上互斥鎖以後,其他的呼叫方不能獲得鎖,只有當互斥鎖的持有方釋放鎖之後其他呼叫方才能獲得鎖。
呼叫方在獲得鎖的時候發現互斥鎖已經被其他方持有,那麼該呼叫方只能進入睡眠狀態,這樣不會佔用CPU資源。但是會有時間的消耗,系統的執行時基於CPU時間排程的,每次執行緒可能有100ms的執行時間,頻繁的CPU切換也會消耗一定的時間。

NSLock:

NSLock遵循NSLocking協議,同時也是互斥鎖,提供了lockunlock方法來進行加鎖和解鎖。 NSLock內部是封裝了pthread_mutext,型別是PTHREAD_MUTEXT_ERRORCHECK,它會損失一定的效能換來錯誤提示。

- (void)lock;  
- (void)unlock; 
- (BOOL)tryLock;  
- (BOOL)lockBeforeDate:(NSDate *)limit;  
複製程式碼

tryLocklock方法都會請求加鎖,唯一不同的是trylock在沒有獲得鎖的時候可以繼續做一些任務和處理。lockBeforeDate:方法也比較簡單,就是在limit時間點之前獲得鎖,沒有拿到鎖就返回NO。

@synchronized:

這其實是一個 OC 層面的鎖,防止不同的執行緒同時執行同一段程式碼,相比於使用 NSLock 建立鎖物件、加鎖和解鎖來說,@synchronized用著更方便,可讀性更高。
大體上,想要明白@synchronized,需要知道在@synchronizedobjc_sync_enterobjc_sync_exit 的成對呼叫,而且每個傳入的物件,都會為其分配一個遞迴鎖並儲存在雜湊表中。在objc_sync_enter中加鎖,在objc_sync_exit 中解鎖。
具體可以參考這篇文章: 關於 @synchronized,這兒比你想知道的還要多

@synchronized(self) {  
    //資料操作  
}
複製程式碼

自旋鎖

自旋鎖的目的是為了確保臨界區只有一個執行緒可以訪問。
當一個執行緒獲得鎖之後,其他執行緒將會一直迴圈在哪裡檢視是否該鎖被釋放,是用於多執行緒同步的一種鎖,執行緒反覆檢查鎖變數是否可用。由於執行緒在這一過程中保持執行,因此是一種忙等待。一旦獲取了自旋鎖,執行緒會一直保持該鎖,直至顯式釋放自旋鎖。自旋鎖避免了程式上下文的排程開銷,因此對於執行緒只會阻塞很短時間的場合是有效的。
由於呼叫方會一直迴圈看該自旋鎖的的保持者是否已經釋放了資源,所以總的效率來說比互斥鎖高。但是自旋鎖只用於短時間的資源訪問,如果不能短時間內獲得鎖,就會一直佔用著CPU,造成效率低下。

OSSpinLock:

自旋鎖的一種,由於在某些場景下不安全已被棄用。 需匯入標頭檔案#import <libkern/OSAtomic.h>

OSSpinLock lock = OS_SPINLOCK_INIT;  
OSSpinLockLock(&lock);  
OSSpinLockUnlock(&lock);
OSSpinLockTry(&lock);
複製程式碼

自旋鎖存在優先順序反轉問題OSSpinLock是自旋鎖,也正是由於它是自旋鎖,所以容易發生優先順序反轉的問題。在ibireme的文章中已經寫到,當一個低優先順序執行緒獲得鎖的時候,如果此時一個高優先順序的系統到來,那麼會進入忙等狀態,不會進入睡眠,此時會一直佔用著系統CPU時間,導致低優先順序的無法拿到CPU時間片,從而無法完成任務也無法釋放鎖。除非能保證訪問鎖的執行緒全部處於同一優先順序,否則系統所有的自旋鎖都會出現優先順序反轉的問題。現在蘋果的OSSpinLock已經被替換成

os_unfair_lock 
typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);
複製程式碼

os_unfair_lock(OSSpinLock 替代品):

os_unfair_lock 是蘋果官方推薦的替換OSSpinLock的方案,用於解決OSSpinLock優先順序反轉問題,但是它在iOS10.0以上的系統才可以呼叫。
os_unfair_lock 非自旋鎖,是一個互斥鎖,引用SoC兄的文章os_unfair_lock的型別和自旋鎖與互斥鎖的比較,包括在在apple的官方文件裡面也是寫明的了,“ This function doesn't spin on contention, but instead waits in the kernel to be awoken by an unlock”,文中只是指出os_unfair_lock 是蘋果推出替代 OSSpinLock的一種相對高效的鎖,並不是說其是自旋鎖。
匯入標頭檔案 #import< os/lock.h >

os_unfair_lock_t unfairLock;  
unfairLock = &(OS_UNFAIR_LOCK_INIT);  
os_unfair_lock_lock(unfairLock);  
os_unfair_lock_unlock(unfairLock);
os_unfair_lock_trylock(unfairLock);
複製程式碼

遞迴鎖

需要使用遞迴鎖的情況:

NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    static void (^RecursiveLock)(int);
    RecursiveLock = ^(int value) {
        [lock lock];
        if (value > 0) {
            NSLog(@"value = %d", value);
            sleep(2);
            RecursiveLock(value - 1);
        }
        [lock unlock];
    };
    RecursiveLock(5);
});
複製程式碼

這段程式碼是一個典型的死鎖情況。在我們的執行緒中,RecursiveMethod是遞迴呼叫的。所有每次進入這個block時,都會去加一次鎖,而從第二次開始,由於鎖已經被使用了且沒有解鎖,所有它需要等待鎖被解除,這樣就導致了死鎖,執行緒被阻塞住了。導致crach

*** -[NSLock lock]: deadlock ( '(null)')   *** Break on _NSLockError() to debug.
複製程式碼

NSRecursiveLock

遞迴鎖也是通過 pthread_mutex_lock函式來實現,在函式內部會判斷鎖的型別,如果顯示是遞迴鎖,就允許遞迴呼叫,僅僅將一個計數器加一,鎖的釋放過程也是同理。
一個鎖可以被同一執行緒多次請求,而不會引起死鎖。這主要是用在迴圈或遞迴操作中。 NSRecursiveLockNSLock的區別在於內部封裝的pthread_mutex_t 物件的型別不同,前者的型別為 PTHREAD_MUTEX_RECURSIVE
主要操作:

  • NSRecursiveLock *lock = [[NSRecursiveLock alloc] init]; // 建立遞迴鎖
  • [lock lockBeforeDate:date];// 在給定的時間之前去嘗試請求一個鎖
  • [lock tryLock];// 嘗試去請求一個鎖,並會立即返回一個布林值,表示嘗試是否成功

另外,NSRecursiveLock還宣告瞭一個name屬性,如下:
@property(copy) NSString *name
我們可以使用這個字串來標識一個鎖。Cocoa也會使用這個name作為錯誤描述資訊的一部分。

條件鎖

NSCondition

封裝了一個互斥鎖和訊號量,它把前者的lock以及後者的wait/signal統一到NSCondition物件中,是基於條件變數pthread_cond_t來實現的,和訊號量相似,如果當前執行緒不滿足條件,那麼就會進入睡眠狀態,等待其他執行緒釋放鎖或者釋放訊號之後,就會喚醒執行緒。
NSCondition 的物件實際上作為一個鎖和一個執行緒檢查器:鎖主要為了當檢測條件時保護資料來源,執行條件引發的任務;執行緒檢查器主要是根據條件決定是否繼續執行執行緒,即執行緒是否被阻塞。

  • NSCondition同樣實現了NSLocking協議,所以它和NSLock一樣,也有NSLocking協議的lockunlock方法,可以當做NSLock來使用解決執行緒同步問題,用法完全一樣。
  • NSCondition提供了waitsignal,和條件訊號量類似。比如我們要監聽array陣列的個數,當array的個數大於0的時候就執行清空操作。思路是這樣的,當array個數大於0時執行清空操作,否則,wait等待執行清空操作。當array個數增加的時候發生signal訊號,讓等待的執行緒喚醒繼續執行。
    NSCondition *lock = [[NSCondition alloc] init];
    NSMutableArray *array = [[NSMutableArray alloc] init];
    //消費者
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        while (!array.count) {
            [lock wait];
        }
        [array removeAllObjects];
        [lock unlock];
    });
    //生產者
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);//以保證讓執行緒2的程式碼後執行
        [lock lock];
        [array addObject:@1];
        [lock signal];
        [lock unlock];
    });
    複製程式碼
  • NSCondition可以給每個執行緒分別加鎖,加鎖後不影響其他執行緒進入臨界區。但是正是因為這種分別加鎖的方式,NSCondition使用wait並使用加鎖後並不能真正的解決資源的競爭。例如:

    不能讓m<0。假設當前m=0,執行緒A要判斷到m>0為假,執行等待;執行緒B執行了m=1操作,並喚醒執行緒A執行m-1操作的同時執行緒C判斷到m>0,因為他們在不同的執行緒鎖裡面,同樣判斷為真也執行了m-1,這個時候執行緒A和執行緒C都會執行m-1,但是m=1,結果就會造成m=-1.

NSCoditionLock

NSConditionLock也可以像NSCondition一樣做多執行緒之間的任務等待呼叫,而且是執行緒安全的。
NSConditionLock同樣實現了NSLocking協議,效能比較低。

NSConditonLock內部持有了一個NSCondition物件和_condition_value屬性,當呼叫

- (instancetype)initWithCondition:(NSInteger)condition

初始化的時候會傳入一個condition引數,該引數會賦值_condition_value屬性

常用方法:
  • lock不分條件,如果鎖沒被申請,直接執行程式碼
  • lockBeforeDate: 在指定時間前嘗試加鎖,返回bool
  • lockWhenCondition:滿足特定條件Condition,加鎖執行相應程式碼
  • lockWhenCondition: beforeDate:和上條相同,增加時間戳
  • tryLock嘗試著加鎖,返回bool
  • tryLockWhenCondition:,滿足特定條件Condition,嘗試著加鎖,返回bool
  • unlock不會清空條件,之後滿足條件的鎖還會執行
  • unlockWithCondition:設定解鎖條件(同一時刻只有一個條件,如果已經設定條件,相當於修改條件)
例項:
- (void)executeNSConditionLock {
    NSConditionLock* lock = [[NSConditionLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (NSUInteger i=0; i<3; i++) {
            sleep(2);
            if (i == 2) {
                [lock lock];
                [lock unlockWithCondition:i];
            }
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCoditionLock:lock];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCoditionLock:lock];
    });
}
-(void)threadMethodOfNSCoditionLock:(NSConditionLock*)lock{
    [lock lockWhenCondition:2];
    [lock unlock];
}
複製程式碼

讀寫鎖

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

pthread_rwlock

讀寫鎖可以有三種狀態:

  • 讀模式下加鎖狀態,
  • 寫模式下加鎖狀態,
  • 不加鎖狀態。

一次只有一個執行緒可以佔有寫模式的讀寫鎖,但是多個執行緒可用同時佔有讀模式的讀寫鎖。讀寫鎖也叫做共享-獨佔鎖,當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的,當它以寫模式鎖住時,它是以獨佔模式鎖住的。
因此:

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

注意:

  • 如果自己已經獲取了讀鎖,再去加寫鎖,會出現死鎖的
// 初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
//獲取一個讀出鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr); 
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
//獲取一個寫入鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr); 
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);

int pthread_rwlock_unlock(pthread_rwlock_t *rwptr); //釋放一個寫入鎖或者讀出鎖


//讀寫鎖屬性:
int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr)
int pthread_rwlock_destroy(pthread_rwlock_t *rwptr);

int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);

int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *valptr);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int valptr);
複製程式碼

例項:

// 初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER
// 讀模式
pthread_rwlock_wrlock(&lock);
// 寫模式
pthread_rwlock_rdlock(&lock);
// 讀模式或者寫模式的解鎖
pthread_rwlock_unlock(&lock);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self readBookWithTag:1];
});
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self readBookWithTag:2];
});
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self writeBook:3];
});
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self writeBook:4];
});
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self readBookWithTag:5];
});
- (void)readBookWithTag:(NSInteger )tag {
    pthread_rwlock_rdlock(&rwLock);
    self.path = [[NSBundle mainBundle] pathForResource:@"1" ofType:@".doc"];
    self.contentString = [NSString stringWithContentsOfFile:self.path encoding:NSUTF8StringEncoding error:nil];
    pthread_rwlock_unlock(&rwLock);
}
- (void)writeBook:(NSInteger)tag {
    pthread_rwlock_wrlock(&rwLock);
    [self.contentString writeToFile:self.path atomically:YES encoding:NSUTF8StringEncoding error:nil];
    pthread_rwlock_unlock(&rwLock);
}
複製程式碼

分散式鎖

NSDistributedLock,分佈鎖,檔案方式實現,可以跨程式 用tryLock方法獲取鎖。 用unlock方法釋放鎖。如果一個獲取鎖的進行在釋放鎖之前掛了,那麼鎖就一直得不到釋放了,此時可以通過breakLock強行獲取鎖。

注:這種鎖在ios上基本用不到,不過多探究。

相關文章