iOS開發基礎——線程安全(線程鎖)

小記

在IOS上進行多線程開發,爲了保證線程安全,防止資源競爭,須要給進程進行加鎖,一般用到的進程鎖分爲7種。ios

  • 信號量
  • 互斥鎖
  • 自旋鎖
  • 遞歸鎖
  • 條件鎖
  • 讀寫鎖
  • 分佈式鎖

鎖:是保證線程安全常見的同步工具,防止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); // 釋放鎖  
複製代碼

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

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切換也會消耗必定的時間。app

NSLock:

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

- (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上基本用不到,不過多探究。

相關文章
相關標籤/搜索