iOS多線程之--線程安全(線程鎖)



iOS多線程demogit

iOS多線程之--NSThreadgithub

iOS多線程之--GCD詳解面試

iOS多線程之--NSOperation數據庫

iOS多線程之--線程安全(線程鎖)安全

iOS多線程相關面試題bash



1. 什麼是線程安全?

若是一段代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。若是每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。通常來講當多個線程訪問同一塊資源(同一個對象、同一個變量、同一個文件)時,很容易引起數據錯亂和數據安全問題。網絡

好比說有個售票系統,開2個線程(至關於2個售票窗口)同時進行售票,售票過程是這樣的:首先從數據庫中取出餘票數量,若是餘票數量大於0,那麼就進行售票,售1張票耗時1秒鐘,出票成功後再將票數減一而後存進數據庫。那2個線程同時售票會有什麼問題呢?假設餘票數量是10,咱們如今模擬一下售票過程:多線程

  • 0.0秒時線程1(窗口1)從數據庫取出餘票數量10,而後進行售票操做(須要耗時1秒)。
  • 0.5秒時線程2(窗口2)從數據庫取出餘票數量10(由於此時窗口1的售票還沒完成,因此數據庫中餘票數仍然是10),而後進行售票操做。
  • 1.0秒時窗口1售票完成,而後將9(10-1=9)存入數據庫,如今數據庫中餘票數量是9。
  • 1.5秒時窗口2售票完成,而後將以前取出的餘票數量10減去1後存入數據庫中,數據庫中餘票數量仍然是9。

從上面已經能夠看出問題了,2個線程總共賣出了2張票,結果數據庫中還剩餘9張票,與咱們的預期不符,因此這不是線程安全的。下面咱們來看實際代碼演示:併發

- (void)noLock{
    self.ticketCount = 10;
    
    // 線程1(窗口1)
    NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
        for (NSInteger i = 0; i < 5; i++) {
            [self noLockSaleTicket];
        }
    }];
    thread1.name = @"窗口1";
    [thread1 start];
    
    // 線程2(窗口2)
    NSThread *thread2 = [[NSThread alloc] initWithBlock:^{
        for (NSInteger i = 0; i < 5; i++) {
            [self noLockSaleTicket];
        }
    }];
    thread2.name = @"窗口2";
    [thread2 start];
}

// 不加鎖時售票過程
- (void)noLockSaleTicket{
    NSInteger oldCount = self.ticketCount; // 取出餘票數量
    if (oldCount > 0) {
        [NSThread sleepForTimeInterval:1.0f]; // 模擬售票耗時1秒
        self.ticketCount = --oldCount; // 售出一張票後更新剩餘票數
    }
    NSLog(@"剩餘票數:%ld--%@",self.ticketCount,[NSThread currentThread]);
}

****************打印結果****************
2020-01-01 10:31:11.260165+0800 MultithreadingDemo[51984:5695718] 剩餘票數:9--<NSThread: 0x60000373b600>{number = 9, name = 窗口2}
2020-01-01 10:31:11.260165+0800 MultithreadingDemo[51984:5695717] 剩餘票數:9--<NSThread: 0x60000373b880>{number = 8, name = 窗口1}
2020-01-01 10:31:12.263126+0800 MultithreadingDemo[51984:5695718] 剩餘票數:8--<NSThread: 0x60000373b600>{number = 9, name = 窗口2}
2020-01-01 10:31:12.263152+0800 MultithreadingDemo[51984:5695717] 剩餘票數:8--<NSThread: 0x60000373b880>{number = 8, name = 窗口1}
2020-01-01 10:31:13.263691+0800 MultithreadingDemo[51984:5695717] 剩餘票數:7--<NSThread: 0x60000373b880>{number = 8, name = 窗口1}
2020-01-01 10:31:13.263693+0800 MultithreadingDemo[51984:5695718] 剩餘票數:7--<NSThread: 0x60000373b600>{number = 9, name = 窗口2}
2020-01-01 10:31:14.264340+0800 MultithreadingDemo[51984:5695717] 剩餘票數:6--<NSThread: 0x60000373b880>{number = 8, name = 窗口1}
2020-01-01 10:31:14.264340+0800 MultithreadingDemo[51984:5695718] 剩餘票數:6--<NSThread: 0x60000373b600>{number = 9, name = 窗口2}
2020-01-01 10:31:15.265322+0800 MultithreadingDemo[51984:5695717] 剩餘票數:5--<NSThread: 0x60000373b880>{number = 8, name = 窗口1}
2020-01-01 10:31:15.265322+0800 MultithreadingDemo[51984:5695718] 剩餘票數:5--<NSThread: 0x60000373b600>{number = 9, name = 窗口2}
複製代碼

2. 如何確保線程安全?

咱們是採用線程同步技術來解決多線程的安全隱患問題,所謂同步,就是協同步調,按預約的前後次序進行。常見的線程同步技術就是加鎖。從上面案例能夠看出,致使出現問題的緣由就是2個線程同時在對數據庫進行讀寫操做,加鎖就是在有線程正在進行讀寫操做時將讀寫操做的代碼鎖住,這樣其餘線程就不能在這個時候進行讀寫操做,等前面的線程完成操做後再解鎖,而後後面的線程才能進行讀寫操做。換句話說加鎖就是讓某段代碼在同一時刻只能有一個線程在執行。異步

在iOS開發中,有多種線程同步方案來確保線程安全,下面來一一介紹:

2.1 OSSpinLock(自旋鎖)

所謂自旋鎖,就是等待鎖的線程是處於忙等狀態。所謂忙等,咱們能夠理解爲就是一個while循環,只要發現鎖是加鎖狀態就一直循環,直到發現鎖處於未加鎖狀態才中止循環。因此自旋鎖一直佔用着CPU資源,不過自旋鎖的效率是很是高的,一旦鎖被釋放,等待鎖的線程立馬就能夠感知到。通常來講,若是被加鎖的代碼塊所需的執行時間很是短就可使用自旋鎖,可是若是是很是耗時的話就不建議使用自旋鎖了,由於等待鎖的線程一直忙等很是耗CPU資源。

自旋鎖的實現原理比較簡單,咱們能夠定義一個BOOL類型變量isLock用來表示鎖的狀態,其初始化爲NO表示未加鎖。若是要加鎖的代碼塊用A表示,當線程1要執行A以前先判斷鎖的狀態是未加鎖狀態,因此線程1獲取到鎖並將isLock置爲YES來加鎖,而後開始執行A代碼塊,執行結束後將isLock置爲NO來解鎖。若是線程1正在執行代碼塊A時線程2也想要執行代碼塊A,結果發現如今是加鎖狀態,因此線程2開始while循環進行忙等,直到線程1解鎖線程2才結束while循環而得到鎖。忙等、加鎖和解鎖能夠用以下的僞代碼來表示:

// while循環進行忙等,循環體裏面什麼都不用作,只要是加鎖狀態就一直循環
while(_isLock){
    
}

// 執行代碼塊A以前加鎖
_isLock = YES;

// 執行代碼塊A
……

// 執行完代碼塊A後解鎖
_isLock = NO;
複製代碼

iOS中的OSSpinLock就是自旋鎖,這是一個C語言實現的鎖,使用時須要導入頭文件#import <libkern/OSAtomic.h>。仍是用前面的售票的例子來進行演示,因爲致使線程安全問題的就是售票過程的代碼塊,因此咱們只需對這一部分代碼進行加鎖解鎖操做。

- (void)OSSpinLockSaleTicket{
    if (!_spinLock) { // 初始化鎖
        _spinLock = OS_SPINLOCK_INIT;
    }
    
    // 加鎖
    OSSpinLockLock(&_spinLock);
    
    NSInteger oldCount = self.ticketCount;
    if (oldCount > 0) {
        [NSThread sleepForTimeInterval:1.0f];
        self.ticketCount = --oldCount;
    }
    NSLog(@"剩餘票數:%ld--%@",self.ticketCount,[NSThread currentThread]);
    
    // 解鎖
    OSSpinLockUnlock(&_spinLock);
}

****************打印結果****************
2020-01-01 17:23:37.832503+0800 MultithreadingDemo[64676:6658463] 剩餘票數:9--<NSThread: 0x600003a54b40>{number = 7, name = 窗口1}
2020-01-01 17:23:38.837188+0800 MultithreadingDemo[64676:6658463] 剩餘票數:8--<NSThread: 0x600003a54b40>{number = 7, name = 窗口1}
2020-01-01 17:23:39.843327+0800 MultithreadingDemo[64676:6658463] 剩餘票數:7--<NSThread: 0x600003a54b40>{number = 7, name = 窗口1}
2020-01-01 17:23:40.848290+0800 MultithreadingDemo[64676:6658463] 剩餘票數:6--<NSThread: 0x600003a54b40>{number = 7, name = 窗口1}
2020-01-01 17:23:41.850766+0800 MultithreadingDemo[64676:6658463] 剩餘票數:5--<NSThread: 0x600003a54b40>{number = 7, name = 窗口1}
2020-01-01 17:23:42.868658+0800 MultithreadingDemo[64676:6658464] 剩餘票數:4--<NSThread: 0x600003a54c80>{number = 8, name = 窗口2}
2020-01-01 17:23:43.872599+0800 MultithreadingDemo[64676:6658464] 剩餘票數:3--<NSThread: 0x600003a54c80>{number = 8, name = 窗口2}
2020-01-01 17:23:44.878190+0800 MultithreadingDemo[64676:6658464] 剩餘票數:2--<NSThread: 0x600003a54c80>{number = 8, name = 窗口2}
2020-01-01 17:23:45.880620+0800 MultithreadingDemo[64676:6658464] 剩餘票數:1--<NSThread: 0x600003a54c80>{number = 8, name = 窗口2}
2020-01-01 17:23:46.885194+0800 MultithreadingDemo[64676:6658464] 剩餘票數:0--<NSThread: 0x600003a54c80>{number = 8, name = 窗口2}
複製代碼

能夠看到加鎖後運行結果就和咱們預期的同樣了。不過這種加鎖方式從iOS10開始就已經被棄用了,由於這種加鎖方式可能會致使出現優先級反轉的問題。所謂優先級反轉,就是可能會出現高優先級任務所需的資源被低優先級任務給鎖住了而致使高優先級任務被阻塞,從而高優先級任務遲遲得不到調度。但其餘中等優先級的任務卻能搶到CPU資源。從現象上來看,好像是中優先級的任務比高優先級任務具備更高的優先權。

2.2 os_unfair_lock

從iOS10開始蘋果再也不建議使用OSSpinLock進行加鎖,取而代之的是用os_unfair_lock進行加鎖。其也是用C語言實現的,使用時須要導入頭文件#import <os/lock.h>。使用方法以下:

- (void)osUnfairLockSaleTicket{
    // 初始化鎖(使用dispatch_once只是爲了保證鎖只被初始化一次)
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _unfairLock = OS_UNFAIR_LOCK_INIT;
    });
    
    // 加鎖
    os_unfair_lock_lock(&_unfairLock);
    
    NSInteger oldCount = self.ticketCount;
    if (oldCount > 0) {
        [NSThread sleepForTimeInterval:1.0f];
        self.ticketCount = --oldCount;
    }
    NSLog(@"os_unfair_lock剩餘票數:%ld--%@",self.ticketCount,[NSThread currentThread]);
    
    // 解鎖
    os_unfair_lock_unlock(&_unfairLock);
}
複製代碼

2.3 pthread_mutex(互斥鎖)

和自旋鎖不一樣的是,等待互斥鎖的線程在等待過程當中是處於休眠狀態。一旦鎖被其餘線程釋放,處於休眠狀態的線程就會被喚醒。因此線程在等待互斥鎖的過程當中是不佔用CPU資源的,可是喚醒線程是須要消耗必定時間的,因此互斥鎖的效率要比自旋鎖低。

pthread_mutex就是一種互斥鎖,它是跨平臺的。使用時須要導入頭文件#import <pthread.h>pthread_mutex使用起來要比前面介紹的那些鎖要麻煩,在初始化鎖以前首先要定義一個鎖的屬性,而後根據鎖的屬性來初始化鎖(初始化鎖時屬性參數能夠直接傳NULL進去,傳NULL就是使用默認類型)。由於pthread_mutex能夠設置幾種不一樣類型的鎖,設置屬性時要指定鎖的類型,鎖的類型包括如下幾種:

// pthread_mutex互斥鎖屬性的類型
 #define PTHREAD_MUTEX_NORMAL 0 // 常規的鎖(默認類型)
 #define PTHREAD_MUTEX_ERRORCHECK 1 // 檢查錯誤類型的鎖(通常用不上)
 #define PTHREAD_MUTEX_RECURSIVE 2 // 遞歸類型的鎖
 #define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL // 默認類型爲常規類型
複製代碼

另外,當再也不須要使用鎖時記得將鎖和鎖的屬性釋放(前面介紹的兩種鎖是沒有提供銷燬鎖的API的),釋放方法以下:

// 釋放鎖的屬性
pthread_mutexattr_destroy(&attr);
// 釋放鎖
pthread_mutex_destroy(&mutexLock);
複製代碼

2.3.1 pthread_mutex---常規鎖

像前面介紹的2種鎖都是常規的鎖,下面咱們來看下pthread_mutex的常規鎖要如何使用。

- (void)pthreadMutexLockSaleTicket{
    // 保證鎖只被初始化一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 初始化鎖的屬性
        pthread_mutexattr_t attr; // 建立鎖的屬性
        pthread_mutexattr_init(&attr); // 初始化鎖的屬性
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 設置鎖屬性的類型
        
        // 根據鎖的屬性來初始化鎖(屬性參數傳NULL也是一個常規鎖)
        pthread_mutex_init(&_pthreadMutexLock, &attr);
    });
    
    // 加鎖
     pthread_mutex_lock(&_pthreadMutexLock);
     
     NSInteger oldCount = self.ticketCount;
     if (oldCount > 0) {
         [NSThread sleepForTimeInterval:1.0f];
         self.ticketCount = --oldCount;
     }
     NSLog(@"pthread_mutex剩餘票數:%ld--%@",self.ticketCount,[NSThread currentThread]);
     
     // 解鎖
     pthread_mutex_unlock(&_pthreadMutexLock);
}
複製代碼

2.3.2 pthread_mutex---遞歸鎖

pthread_mutex遞歸鎖pthread_mutex常規鎖在使用方法上是同樣的,只需把屬性的類型參數由PTHREAD_MUTEX_NORMAL改成PTHREAD_MUTEX_RECURSIVE就能夠了。

可是到底什麼是遞歸鎖呢?什麼狀況下須要用到遞歸鎖呢?提及遞歸,咱們首先想到的就是函數的遞歸調用,也就是在函數內部又調用了本身。遞歸鎖主要就是用在函數的遞歸調用場合的,其特色就是鎖裏面又加鎖,換句話說就是同一把鎖能夠重複加屢次。若是在這種場合下咱們用常規鎖會出現什麼問題呢?咱們看下下面這個例子:

- (void)test{
    // 加鎖
     pthread_mutex_lock(&_pthreadMutexNotmalLock);
     
     // 加鎖代碼爲遞歸調用
    static NSInteger i = 5;
    NSInteger temp = i--;
    if (temp > 0) {
        [self test];
    }
    NSLog(@"%ld",temp);
     
     // 解鎖
     pthread_mutex_unlock(&_pthreadMutexNotmalLock);
}
複製代碼

若是某個線程調用上面這個方法的話就會死鎖,不會有任何打印信息。由於這是一個常規鎖,當線程第一次調用test方法時,這個線程獲取到鎖,此時tem>0,會第二次調用test(注意這個時候鎖沒有被釋放),因此第二次調用test時發現鎖此時是加鎖狀態,只能在這裏等鎖釋放後才能繼續日後執行。而第一次調用test又必須等第二次調用test結束了才能繼續往下執行來釋放鎖,這就形成了死鎖。

咱們再來看看換成遞歸鎖會怎麼樣:

- (void)pthreadMutexRecursiveLockTest{
    
    // 保證鎖只被初始化一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 初始化鎖的屬性
        pthread_mutexattr_t attr; // 建立鎖的屬性
        pthread_mutexattr_init(&attr); // 初始化鎖的屬性
//        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 設置鎖屬性的類型爲常規鎖的話就會形成死鎖
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 設置鎖屬性的類型爲遞歸鎖
        
        // 根據鎖的屬性來初始化鎖
        pthread_mutex_init(&_pthreadMutexRecursiveLock, &attr);
    });
    
    // 加鎖
     pthread_mutex_lock(&_pthreadMutexRecursiveLock);
     
     // 加鎖代碼爲遞歸調用
    static NSInteger i = 5;
    NSInteger temp = i--;
    if (temp > 0) {
        [self pthreadMutexRecursiveLockTest];
    }
    NSLog(@"pthread_mutex遞歸鎖---%ld",temp);
     
     // 解鎖
     pthread_mutex_unlock(&_pthreadMutexRecursiveLock);
}

****************打印結果****************
2020-01-02 10:28:09.961983+0800 MultithreadingDemo[59365:5508927] pthread_mutex遞歸鎖---0
2020-01-02 10:28:09.962160+0800 MultithreadingDemo[59365:5508927] pthread_mutex遞歸鎖---1
2020-01-02 10:28:09.962310+0800 MultithreadingDemo[59365:5508927] pthread_mutex遞歸鎖---2
2020-01-02 10:28:09.962407+0800 MultithreadingDemo[59365:5508927] pthread_mutex遞歸鎖---3
2020-01-02 10:28:09.962471+0800 MultithreadingDemo[59365:5508927] pthread_mutex遞歸鎖---4
2020-01-02 10:28:09.962526+0800 MultithreadingDemo[59365:5508927] pthread_mutex遞歸鎖---5
複製代碼

可見換成遞歸鎖後打印結果和咱們預期同樣。那遞歸鎖爲何能夠重複加鎖而不會形成死鎖呢?我這裏提供一個本身實現遞歸鎖的思路:前面介紹自旋鎖時,咱們能夠用一個BOOL類型的變量來記錄鎖加鎖、解鎖狀態,在遞歸鎖裏面咱們還用BOOL類型的變量的話顯然就行不通了。咱們能夠用用一個int類型的變量lockCount(加鎖次數)來記錄鎖的狀態,每加一次鎖lockCount就+1,每次解鎖lockCount就-1,當lockCount爲0時就表示是未加鎖的狀態。固然,咱們還要保證只有同一個線程才能重複加鎖,而其餘線程來訪問的話仍是須要等待鎖的釋放,因此咱們還必須將當前持有鎖的線程給記錄下來。


2.3.3 pthread_mutex---條件鎖

當某線程獲取了鎖對象,但由於某些條件沒有知足,須要在這個條件上等待,直到條件知足纔可以往下繼續執行時,就須要用到條件鎖

舉個例子,遊戲中有產生敵人和殺死敵人2個方法是用同一把鎖進行加鎖的,殺死敵人有個前提條件就是必須有敵人存,若是在殺死敵人的線程獲取到鎖後發現敵人不存在,那這個線程就要等待,等有新的敵人產生了再進行殺死操做。好比下面代碼中的案例,程序先調用了killEnemy方法,後調用createEnemy方法,此時就會出現條件等待。(實際開發中可能並不會這樣設計代碼邏輯,這裏只是爲了講解條件鎖的運行流程才這樣設計。)

- (void)pthreadMutexConditionLockTest{
    [self initPthreadConditionLock]; // 初始化鎖、條件和一些相關數據
    
    // 建立一個線程調用killEnemy方法(此時尚未敵人,因此會進入等待狀態)
    dispatch_queue_t queue = dispatch_queue_create("lock", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"killEnemy開始--%@<##>",[NSThread currentThread]);
        [self killEnemy];
        NSLog(@"killEnemy結束--%@<##>",[NSThread currentThread]);
    });
    
    // 2秒後再建立一個線程調用createEnemy方法來產生敵人
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), queue, ^{
        NSLog(@"createEnemy開始--%@<##>",[NSThread currentThread]);
        [self createEnemy];
        NSLog(@"createEnemy結束--%@<##>",[NSThread currentThread]);
    });
}

// 初始化鎖、條件和一些相關數據(確保只初始化一次)
- (void)initPthreadConditionLock{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        pthread_mutex_init(&_pthreadMutexConditionLock, NULL); // 第二個參數傳NULL時是一個常規鎖
        pthread_cond_init(&_pthreadCondition, NULL); // 初始化條件,第二個參數通常就傳NULL
    });
    
    if (!_enemyArr) {
        _enemyArr = [NSMutableArray array];
    }else{
        [_enemyArr removeAllObjects];
    }
}

// 殺死敵人
- (void)killEnemy{
    // 加鎖
    pthread_mutex_lock(&_pthreadMutexConditionLock);
    
    if (_enemyArr.count == 0) {
        NSLog(@"尚未敵人,進入等待狀態");
        pthread_cond_wait(&_pthreadCondition, &_pthreadMutexConditionLock); // 等待將鎖和條件傳進去
    }
    
    [_enemyArr removeLastObject];
    NSLog(@"殺死了敵人");
    
    // 解鎖
    pthread_mutex_unlock(&_pthreadMutexConditionLock);
}

// 產生敵人
- (void)createEnemy{
    // 加鎖
    pthread_mutex_lock(&_pthreadMutexConditionLock);
    
    NSObject *enemyObj = [NSObject new];
    [_enemyArr addObject:enemyObj];
    
    // 發送信號喚醒一條等待條件的線程(發送信號的代碼放在解鎖代碼的前面和後面均可以,放在前面的話就是先發信號喚醒等待的線程,等解鎖後等待的線程才能得到鎖;放在後面的話當前線程就會先解鎖,而後發送信號喚醒等待的線程,等待的線程立馬就能夠得到鎖。)
    pthread_cond_signal(&_pthreadCondition);
//    pthread_cond_broadcast(&_pthreadCondition); // 發送廣播喚醒全部等待條件的線程
    
    if(_enemyArr.count == 1) NSLog(@"敵人數從0變爲1,喚醒等待中的線程。");
    
    // 解鎖
    pthread_mutex_unlock(&_pthreadMutexConditionLock);
}

****************打印結果****************
2020-01-02 12:10:21.386852+0800 MultithreadingDemo[59656:5546517] killEnemy開始--<NSThread: 0x60000026ac00>{number = 3, name = (null)}<##>
2020-01-02 12:10:21.386984+0800 MultithreadingDemo[59656:5546517] 尚未敵人,進入等待狀態
2020-01-02 12:10:23.386917+0800 MultithreadingDemo[59656:5546516] createEnemy開始--<NSThread: 0x6000002eb240>{number = 6, name = (null)}<##>
2020-01-02 12:10:23.387074+0800 MultithreadingDemo[59656:5546516] 敵人數從0變爲1,喚醒等待中的線程。
2020-01-02 12:10:23.387217+0800 MultithreadingDemo[59656:5546516] createEnemy結束--<NSThread: 0x6000002eb240>{number = 6, name = (null)}<##>
2020-01-02 12:10:23.387219+0800 MultithreadingDemo[59656:5546517] 殺死了敵人
2020-01-02 12:10:23.387329+0800 MultithreadingDemo[59656:5546517] killEnemy結束--<NSThread: 0x60000026ac00>{number = 3, name = (null)}<##>
複製代碼

下面咱們來說解一下上面代碼的運行流程(關於一些初始化操做就不在這裏說了),咱們把調用killEnemy方法的線程叫killThread,把調用createEnemy的線程叫createThread線程。

  • 首先調用killEnemy方法,killThread線程獲取到鎖。
  • 而後判斷髮現如今尚未敵人,不能進行kill操做,因此killThread釋放鎖並進入等待狀態。
  • 2秒後調用createEnemy方法,createThread發現鎖是未加鎖狀態,因此獲取到了鎖並執行生成敵人的操做。
  • 生成敵人後就會給條件發送信號(同時createThread線程會釋放鎖)。
  • killThread線程收到條件信號後被喚醒並從新獲取到鎖。
  • killThread線程被喚醒後開始執行條件等待以後的代碼,等殺死敵人後將鎖釋放。

那麼實際開發中什麼場合下會用到條件鎖呢?

咱們能夠用條件鎖來創建線程中的依賴關係。好比用2個線程去執行用戶登陸和獲取用戶信息這兩個網絡請求(用戶登陸成功後會返回一個token,須要用這個token去獲取用戶信息,也就是說獲取用戶信息是依賴於用戶登陸的),這兩個請求的執行前後順序是不肯定的,因此就有可能出現執行請求用戶信息操做時用戶都尚未登陸,那麼這個時候就可使用條件鎖進行等待,等登陸成功返回token後再發送信號將其喚醒。

2.4 NSLock

前面介紹的鎖都是C語言實現的,在iOS開發中使用的並非不少,咱們更多使用的是OC封裝的鎖。NSLock就是OC對pthread_mutex常規鎖的封裝。使用起來比較簡單,以下所示:

- (void)nsLockSaleTicket{
    // 初始化鎖
    if (!_nsLock) {
        _nsLock = [[NSLock alloc] init];
    }
    
    // 加鎖
    [_nsLock lock];
    
    NSInteger oldCount = self.ticketCount;
    if (oldCount > 0) {
        [NSThread sleepForTimeInterval:1.0f];
        self.ticketCount = --oldCount;
    }
    NSLog(@"NSLock剩餘票數:%ld--%@",self.ticketCount,[NSThread currentThread]);
    
    // 解鎖
    [_nsLock unlock];
}
複製代碼

NSLock還有下面兩個方法:

/* 
嘗試加鎖
若是獲取到了鎖就加鎖並返回YES
若是如今鎖被另一個線程持有,那就返回NO,並且不會在這裏等待鎖的釋放,而是會繼續執行後面的代碼。
*/
- (BOOL)tryLock;

/*
在某個時間以前嘗試加鎖
若是在這個時間以前獲取到了鎖就加鎖並返回YES
若是到這個時間點還沒獲取到鎖就返回NO,並且不會在這裏等待鎖的釋放,而是會繼續執行後面的代碼。
*/
- (BOOL)lockBeforeDate:(NSDate *)limit;
複製代碼

2.5 NSRecursiveLock(遞歸鎖)

NSRecursiveLock是一個遞歸鎖,是OC對pthread_mutex遞歸鎖的封裝。NSRecursiveLock的API調用方法和NSLock是同樣的。

- (void)nsRecursiveLockTest{
    if (!_nsRecursiveLock) { // 初始化鎖
        _nsRecursiveLock = [[NSRecursiveLock alloc] init];
    }
    
    // 加鎖
    [_nsRecursiveLock lock];
    
     // 加鎖代碼爲遞歸調用
    static NSInteger i = 5;
    NSInteger temp = i--;
    if (temp > 0) {
        [self nsRecursiveLockTest];
    }
    NSLog(@"NSRecursiveLock 遞歸鎖---%ld",temp);
     
     // 解鎖
     [_nsRecursiveLock unlock];
}
複製代碼

2.6 NSCondition(條件鎖)

NSCondition是一個條件鎖,是OC對pthread_mutex條件鎖的封裝。NSCondition的API和使用方式和pthread_mutex條件鎖是同樣的,將前面條件鎖的案例換成NSCondition,代碼以下:

- (void)nsConditionTest{
    // 初始化鎖
    if (!_nsCondition) {
        _nsCondition = [[NSCondition alloc] init];
        _enemyArr = [NSMutableArray array];
    }
    
    // 建立一個線程調用killEnemy1方法(此時尚未敵人,因此會進入等待狀態)
    dispatch_queue_t queue = dispatch_queue_create("NSCondition", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"killEnemy1開始--%@<##>",[NSThread currentThread]);
        [self killEnemy1];
        NSLog(@"killEnemy1結束--%@<##>",[NSThread currentThread]);
    });
    
    // 2秒後再建立一個線程調用createEnemy1方法來產生敵人
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), queue, ^{
        NSLog(@"createEnemy1開始--%@<##>",[NSThread currentThread]);
        [self createEnemy1];
        NSLog(@"createEnemy1結束--%@<##>",[NSThread currentThread]);
    });
}

// 殺死敵人
- (void)killEnemy1{
    // 加鎖
    [_nsCondition lock];
    
    if (_enemyArr.count == 0) {
        NSLog(@"尚未敵人,進入等待狀態");
        [_nsCondition wait]; // 等待
    }
    
    [_enemyArr removeLastObject];
    NSLog(@"殺死了敵人");
    
    // 解鎖
    [_nsCondition unlock];
}

// 產生敵人
- (void)createEnemy1{
    // 加鎖
    [_nsCondition lock];
    
    NSObject *enemyObj = [NSObject new];
    [_enemyArr addObject:enemyObj];
    
    // 發送信號喚醒一條等待條件的線程
    [_nsCondition signal];
//    [_nsCondition broadcast]; // 發送廣播喚醒全部等待條件的線程
    
    if(_enemyArr.count == 1) NSLog(@"敵人數從0變爲1,喚醒等待中的線程。");
    
    // 解鎖
    [_nsCondition unlock];
}
複製代碼

2.7 NSConditionLock(條件鎖)

NSConditionLock也是一個條件鎖,它是對NSCondition的進一步封裝,與NSCondition不一樣的是NSConditionLock能夠設置條件值。咱們能夠經過設置不一樣的條件值來創建不一樣線程之間的依賴關係。好比下面案例中獲取用戶信息操做要依賴用戶登錄操做,其加鎖流程以下:

  • 首先初始化鎖時設置的條件值是1(條件值不設置的話默認是0);
  • 而後開啓一個線程執行獲取用戶信息操做,獲取用信息的線程執行[_nsConditionLock lockWhenCondition:2],這句代碼的意思是當條件值是2時就獲取鎖進行加鎖,因爲初始化的條件值是1,因此加鎖失敗,該線程進入等待狀態,等條件值變爲2時就會被喚醒。
  • 1秒鐘後又建立了一個線程執行用戶登錄操做,登錄操做的線程執行[_nsConditionLock lockWhenCondition:1],而此時條件值正好是1,因此加鎖成功,開始登錄,登錄成功後執行[_nsConditionLock unlockWithCondition:2],意思是當前線程解鎖,並將條件設置爲2。
  • 條件值變爲2後獲取用信息的線程從等待狀態變被喚醒並加鎖,開始獲取用戶信息,獲取用戶信息結束後解鎖,整個流程就結束了。
- (void)nsConditionLockTest{
    // 用條件值初始化鎖(也能夠直接[[NSConditionLock alloc] init]來初始化,這樣初始化的條件值是0)
    if(!_nsConditionLock){
        _nsConditionLock = [[NSConditionLock alloc] initWithCondition:1];
    }

    // 開啓一個線程執行獲取用戶信息操做
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"開始獲取用戶信息");
        [self getUserInfoTest];
        NSLog(@"獲取用戶信息結束");
    });
    
    // 1秒鐘後開啓另外一個線程執行登錄操做
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0f * NSEC_PER_SEC)), queue, ^{
        NSLog(@"開始登錄");
        [self loginTest];
        NSLog(@"結束登錄");
    });
}

// 模擬登錄
- (void)loginTest{
    [_nsConditionLock lockWhenCondition:1];
    
    [NSThread sleepForTimeInterval:1.0f];
    NSLog(@"登錄成功");
    
    [_nsConditionLock unlockWithCondition:2];
}

// 模擬獲取用戶信息
- (void)getUserInfoTest{
    [_nsConditionLock lockWhenCondition:2];
    
    [NSThread sleepForTimeInterval:1.0f];
    NSLog(@"獲取用戶信息成功");
    
    [_nsConditionLock unlock];
}

****************打印結果****************
2020-01-02 22:04:39.682835+0800 MultithreadingDemo[90902:8539476] 開始獲取用戶信息
2020-01-02 22:04:40.682996+0800 MultithreadingDemo[90902:8539474] 開始登錄
2020-01-02 22:04:41.684167+0800 MultithreadingDemo[90902:8539474] 登錄成功
2020-01-02 22:04:41.684664+0800 MultithreadingDemo[90902:8539474] 結束登錄
2020-01-02 22:04:42.689556+0800 MultithreadingDemo[90902:8539476] 獲取用戶信息成功
2020-01-02 22:04:42.689911+0800 MultithreadingDemo[90902:8539476] 獲取用戶信息結束
複製代碼

上面咱們都是經過[_nsConditionLock lockWhenCondition:1]來加鎖,這種方式要等到條件值知足要求時才能加鎖成功。咱們也能夠直接經過[_nsConditionLock lock]這種方式來加鎖,這種方式無論條件值是多少均可以加鎖成功。

2.8 串行隊列實現線程同步

線程同步的本質就是保證同一時間只有一個線程訪問要加鎖的代碼塊,那麼咱們能夠將要加鎖的代碼塊當作成一個任務同步添加到GCD的串行隊列中,串行隊列能夠保證一次只有一個任務在執行,前一個任務執行完了才能執行下一個任務。代碼以下:

- (void)serialQueueSaleTicket{
    if (!_serialQueue) {
        _serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    }
    
    dispatch_sync(_serialQueue, ^{ // 要加鎖的代碼塊當成任務同步添加到隊列中
        NSInteger oldCount = self.ticketCount;
        if (oldCount > 0) {
            [NSThread sleepForTimeInterval:1.0f];
            self.ticketCount = --oldCount;
        }
        NSLog(@"串行隊列--剩餘票數:%ld--%@",self.ticketCount,[NSThread currentThread]);
    });
}


****************打印結果****************
2020-01-02 23:14:34.094617+0800 MultithreadingDemo[1146:25021] 串行隊列--剩餘票數:9--<NSThread: 0x6000021f5700>{number = 6, name = 窗口1}
2020-01-02 23:14:35.096506+0800 MultithreadingDemo[1146:25022] 串行隊列--剩餘票數:8--<NSThread: 0x6000021f5800>{number = 7, name = 窗口2}
2020-01-02 23:14:36.099239+0800 MultithreadingDemo[1146:25021] 串行隊列--剩餘票數:7--<NSThread: 0x6000021f5700>{number = 6, name = 窗口1}
2020-01-02 23:14:37.099940+0800 MultithreadingDemo[1146:25022] 串行隊列--剩餘票數:6--<NSThread: 0x6000021f5800>{number = 7, name = 窗口2}
2020-01-02 23:14:38.103534+0800 MultithreadingDemo[1146:25021] 串行隊列--剩餘票數:5--<NSThread: 0x6000021f5700>{number = 6, name = 窗口1}
2020-01-02 23:14:39.105606+0800 MultithreadingDemo[1146:25022] 串行隊列--剩餘票數:4--<NSThread: 0x6000021f5800>{number = 7, name = 窗口2}
2020-01-02 23:14:40.107929+0800 MultithreadingDemo[1146:25021] 串行隊列--剩餘票數:3--<NSThread: 0x6000021f5700>{number = 6, name = 窗口1}
2020-01-02 23:14:41.111818+0800 MultithreadingDemo[1146:25022] 串行隊列--剩餘票數:2--<NSThread: 0x6000021f5800>{number = 7, name = 窗口2}
2020-01-02 23:14:42.117835+0800 MultithreadingDemo[1146:25021] 串行隊列--剩餘票數:1--<NSThread: 0x6000021f5700>{number = 6, name = 窗口1}
2020-01-02 23:14:43.119191+0800 MultithreadingDemo[1146:25022] 串行隊列--剩餘票數:0--<NSThread: 0x6000021f5800>{number = 7, name = 窗口2}
複製代碼

2.9 dispatch_semaphore_t(信號量)

dispatch_semaphore_t是GCD中用於控制最大併發數的,其初始化時設置的信號量值就是最大併發數,當信號量值初始化爲1時表示最大併發數爲1,也就達到了同一時間只有一個線程訪問要加鎖代碼塊的目的,從而實現代碼同步。

// dispatch_semaphore_wait()和dispatch_semaphore_signal()之間的代碼就是要加鎖的代碼
- (void)dispatchSemaphoreSaleTicket{
    if (!_semaphore) { // 初始化信號量爲1,表最大併發數爲1
        _semaphore = dispatch_semaphore_create(1);
    }
    
    // 若是信號量>0,則信號量-1並執行後面代碼
    // 若是信號量<=0,則線程進入等待狀態(線程休眠),第二個參數是等待時間,等信號量大於0或者等待超時時開始執行後續代碼(DISPATCH_TIME_FOREVER表示永不超時)
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
    
    NSInteger oldCount = self.ticketCount;
    if (oldCount > 0) {
        [NSThread sleepForTimeInterval:1.0f];
        self.ticketCount = --oldCount;
    }
    NSLog(@"信號量--剩餘票數:%ld--%@",self.ticketCount,[NSThread currentThread]);
    
    // 執行dispatch_semaphore_signal()函數信號量+1
    dispatch_semaphore_signal(_semaphore);
}

****************打印結果****************
2020-01-02 23:33:14.201100+0800 MultithreadingDemo[1454:34572] 信號量--剩餘票數:9--<NSThread: 0x600002064500>{number = 7, name = 窗口1}
2020-01-02 23:33:15.202743+0800 MultithreadingDemo[1454:34573] 信號量--剩餘票數:8--<NSThread: 0x600002064600>{number = 8, name = 窗口2}
2020-01-02 23:33:16.208335+0800 MultithreadingDemo[1454:34572] 信號量--剩餘票數:7--<NSThread: 0x600002064500>{number = 7, name = 窗口1}
2020-01-02 23:33:17.213433+0800 MultithreadingDemo[1454:34573] 信號量--剩餘票數:6--<NSThread: 0x600002064600>{number = 8, name = 窗口2}
2020-01-02 23:33:18.218910+0800 MultithreadingDemo[1454:34572] 信號量--剩餘票數:5--<NSThread: 0x600002064500>{number = 7, name = 窗口1}
2020-01-02 23:33:19.224498+0800 MultithreadingDemo[1454:34573] 信號量--剩餘票數:4--<NSThread: 0x600002064600>{number = 8, name = 窗口2}
2020-01-02 23:33:20.230172+0800 MultithreadingDemo[1454:34572] 信號量--剩餘票數:3--<NSThread: 0x600002064500>{number = 7, name = 窗口1}
2020-01-02 23:33:21.235774+0800 MultithreadingDemo[1454:34573] 信號量--剩餘票數:2--<NSThread: 0x600002064600>{number = 8, name = 窗口2}
2020-01-02 23:33:22.237456+0800 MultithreadingDemo[1454:34572] 信號量--剩餘票數:1--<NSThread: 0x600002064500>{number = 7, name = 窗口1}
2020-01-02 23:33:23.238408+0800 MultithreadingDemo[1454:34573] 信號量--剩餘票數:0--<NSThread: 0x600002064600>{number = 8, name = 窗口2}
複製代碼

2.10 @synchronized

@synchronized是使用起來最簡單的鎖,寫法很簡單:@synchronized (obj) {}。它的底層是對pthread_mutex遞歸鎖的封裝,其內部會根據小括號傳入的對象生成對應的遞歸鎖,而後進行加鎖解鎖操做。不過這個鎖雖然寫起來方便,可是性能比較差。

- (void)synchronizedTest{
    @synchronized (self) {
        // 加鎖代碼爲遞歸調用
           static NSInteger i = 5;
           NSInteger temp = i--;
           if (temp > 0) {
               [self nsRecursiveLockTest];
           }
           NSLog(@"NSRecursiveLock 遞歸鎖---%ld",temp);
    }
}

****************打印結果****************
2020-01-02 23:55:14.626155+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 遞歸鎖---0
2020-01-02 23:55:14.626455+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 遞歸鎖---1
2020-01-02 23:55:14.626658+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 遞歸鎖---2
2020-01-02 23:55:14.626836+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 遞歸鎖---3
2020-01-02 23:55:14.627004+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 遞歸鎖---4
2020-01-02 23:55:14.627160+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 遞歸鎖---5
2020-01-02 23:55:14.627601+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 遞歸鎖---5
複製代碼

2.11 iOS線程同步方案小結

iOS線程同步方案性能比較:

性能從高到低排序:

  • os_unfair_lock
  • OSSpinLock
  • dispatch_semaphore
  • pthread_mutex
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSCondition
  • pthread_mutex(recursive)
  • NSRecursiveLock
  • NSConditionLock
  • @synchronized

什麼狀況使用自旋鎖比較划算?

  • 預計線程等待鎖的時間很短
  • 加鎖的代碼(臨界區)常常被調用,但競爭狀況不多發生
  • CPU資源不緊張
  • 多核處理器

什麼狀況使用互斥鎖比較划算?

  • 預計線程等待鎖的時間較長
  • 單核處理器
  • 臨界區有IO操做(文件操做)
  • 臨界區代碼複雜或者循環量大
  • 臨界區競爭很是激烈

3. iOS中的讀寫安全方案

當多個線程同時訪問一個資源時容易形成數據錯亂,其根本緣由就在於寫的操做上,由於在沒有進行寫操做時,同一個數據不管同時讀多少次獲得的結果都是同樣的。並且讀操做和寫操做是不能同時進行的。因此爲了保證讀寫安全,必須知足如下3個條件:

  • 同一時間,只能有1個線程進行寫的操做
  • 同一時間,容許有多個線程進行讀的操做
  • 同一時間,不容許既有寫的操做,又有讀的操做

上面的場景就是典型的「多讀單寫」,常常用於文件等數據的讀寫操做,iOS中的實現方案有pthread_rwlock(讀寫鎖)和GCD的dispatch_barrier_async(異步柵欄函數)。

3.1 pthread_rwlock(讀寫鎖)

pthread_rwlock須要引入頭文件#import <pthread.h>,等待鎖的線程會進入休眠。下面是相關的API:

// 初始化鎖
    pthread_rwlock_t lock;
    pthread_rwlock_init(&lock, NULL);
    
    // 讀數據時加鎖
    pthread_rwlock_rdlock(&lock);
    // 讀數據時嘗試加鎖
    pthread_rwlock_tryrdlock(&lock);
    
    // 寫數據時加鎖
    pthread_rwlock_wrlock(&lock);
    // 寫數據時嘗試加鎖
    pthread_rwlock_trywrlock(&lock);
    
    // 解鎖
    pthread_rwlock_unlock(&lock);
    
    // 銷燬鎖
    pthread_rwlock_destroy(&lock);
複製代碼

下面咱們來看下實際代碼演示:

- (void)pthreadRwlockTest{

   // 初始化鎖
   pthread_rwlock_init(&_pthreadRwlock, NULL);
   
   dispatch_queue_t queue = dispatch_queue_create("rwlock", DISPATCH_QUEUE_CONCURRENT);
   
   for (NSInteger i = 0; i < 2; i++) {
       dispatch_async(queue, ^{
           [self read];
           [self read];
           [self write];
           [self write];
           [self read];
           [self read];
       });
   }
}

// 讀操做
- (void)read{
   // 讀加鎖
   pthread_rwlock_rdlock(&_pthreadRwlock);
   
   [NSThread sleepForTimeInterval:1.0f];
   NSLog(@"讀操做");
   
   // 解鎖
   pthread_rwlock_unlock(&_pthreadRwlock);
}

// 寫操做
- (void)write{
   // 寫加鎖
   pthread_rwlock_wrlock(&_pthreadRwlock);
   
   [NSThread sleepForTimeInterval:1.0f];
   NSLog(@"寫操做");
   
   // 解鎖
   pthread_rwlock_unlock(&_pthreadRwlock);
}

// ****************打印結果****************
2020-01-03 09:33:53.220426+0800 MultithreadingDemo[61575:5891181] 讀操做
2020-01-03 09:33:53.220490+0800 MultithreadingDemo[61575:5891182] 讀操做
2020-01-03 09:33:54.221545+0800 MultithreadingDemo[61575:5891182] 讀操做
2020-01-03 09:33:54.221570+0800 MultithreadingDemo[61575:5891181] 讀操做
2020-01-03 09:33:55.225951+0800 MultithreadingDemo[61575:5891182] 寫操做
2020-01-03 09:33:56.231264+0800 MultithreadingDemo[61575:5891181] 寫操做
2020-01-03 09:33:57.231891+0800 MultithreadingDemo[61575:5891182] 寫操做
2020-01-03 09:33:58.233379+0800 MultithreadingDemo[61575:5891181] 寫操做
2020-01-03 09:33:59.235486+0800 MultithreadingDemo[61575:5891181] 讀操做
2020-01-03 09:33:59.235522+0800 MultithreadingDemo[61575:5891182] 讀操做
2020-01-03 09:34:00.237975+0800 MultithreadingDemo[61575:5891181] 讀操做
2020-01-03 09:34:00.237974+0800 MultithreadingDemo[61575:5891182] 讀操做
複製代碼

3.2 dispatch_barrier_async(異步柵欄函數)

GCD中dispatch_barrier_async的特色就是它不會阻塞當前線程,可是它會阻塞同一個派發隊列中在它以後的任務的執行。所以咱們能夠將寫的操做放在異步柵欄函數中,讀操做放在普通的GCD異步函數中。要注意的是,要寫柵欄函數生效,必須相關任務都放在同一個派發隊列中,而且要是本身建立的併發隊列。代碼以下所示:

- (void)dispatchBarrierAsync{
   _readWriteQueue = dispatch_queue_create("readWriteQueue", DISPATCH_QUEUE_CONCURRENT);
   
   for (NSInteger i = 0; i < 2; i++) {
       dispatch_async(_readWriteQueue, ^{
           [self read1];
           [self read1];
           [self write1];
           [self write1];
           [self read1];
           [self read1];
       });
   }
}

// 讀操做
- (void)read1{
   dispatch_async(_readWriteQueue, ^{
       [NSThread sleepForTimeInterval:1.0f];
       NSLog(@"讀操做");
   });
}

// 寫操做
- (void)write1{
   dispatch_barrier_async(_readWriteQueue, ^{
       [NSThread sleepForTimeInterval:1.0f];
       NSLog(@"寫操做");
   });
}

// ****************打印結果****************
2020-01-03 09:52:23.311609+0800 MultithreadingDemo[61639:5898905] 讀操做
2020-01-03 09:52:23.311593+0800 MultithreadingDemo[61639:5898906] 讀操做
2020-01-03 09:52:24.316886+0800 MultithreadingDemo[61639:5898905] 寫操做
2020-01-03 09:52:25.319288+0800 MultithreadingDemo[61639:5898905] 寫操做
2020-01-03 09:52:26.321448+0800 MultithreadingDemo[61639:5898905] 讀操做
2020-01-03 09:52:26.321448+0800 MultithreadingDemo[61639:5898908] 讀操做
2020-01-03 09:52:26.321448+0800 MultithreadingDemo[61639:5898906] 讀操做
2020-01-03 09:52:26.321452+0800 MultithreadingDemo[61639:5898907] 讀操做
2020-01-03 09:52:27.325920+0800 MultithreadingDemo[61639:5898907] 寫操做
2020-01-03 09:52:28.330862+0800 MultithreadingDemo[61639:5898907] 寫操做
2020-01-03 09:52:29.333543+0800 MultithreadingDemo[61639:5898907] 讀操做
2020-01-03 09:52:29.333543+0800 MultithreadingDemo[61639:5898908] 讀操做
複製代碼
相關文章
相關標籤/搜索