1. 程式人生 > >iOS執行緒鎖探究

iOS執行緒鎖探究

多執行緒在我們開發中,被廣泛應用,讓app的效能得到了很大的提高,但是在一些應用場景卻會問題。比如一個電影院中有9張電影票,開設了三個售票視窗,他們同時開始售票,程式碼如下:

#import "ViewController.h"
CGFloat totalTicket = 9;
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i=0; i<totalTicket; i++) {
            [self saleTicketsMether];
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i=0; i<totalTicket; i++) {
            [self saleTicketsMether];
        }
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i=0; i<totalTicket; i++) {
            [self saleTicketsMether];
        }
    });
}

-(void)saleTicketsMether{
    if (totalTicket>0) {
        sleep(0.1);
        totalTicket--;
        NSLog(@"totalTicket:%f",totalTicket);
    }
}

列印輸出結果:

2018-10-30 12:01:55.049887+0800 執行緒鎖-18-10-30-0[2783:84995] totalTicket:7.000000

2018-10-30 12:01:55.049887+0800 執行緒鎖-18-10-30-0[2783:84998] totalTicket:8.000000

2018-10-30 12:01:55.049888+0800 執行緒鎖-18-10-30-0[2783:84997] totalTicket:6.000000

2018-10-30 12:01:55.050008+0800 執行緒鎖-18-10-30-0[2783:84995] totalTicket:3.000000

2018-10-30 12:01:55.050007+0800 執行緒鎖-18-10-30-0[2783:84997] totalTicket:5.000000

2018-10-30 12:01:55.050007+0800 執行緒鎖-18-10-30-0[2783:84998] totalTicket:4.000000

2018-10-30 12:01:55.050059+0800 執行緒鎖-18-10-30-0[2783:84995] totalTicket:2.000000

從結果中,我們可以看出他的輸出完全是亂序的,並不是正常我們認知下遞減結果,是不安全的執行緒。

一、什麼是執行緒安全?

       多執行緒操作共享資料不會出現想不到的結果就是執行緒安全的,否則,是執行緒不安全的。比如:多個執行緒同時訪問或讀取同一共享資料,每個執行緒的讀到的資料都是一樣的,也就不存線上程不安全。如果多個執行緒對同一資源進行讀寫操作,那麼每個執行緒讀到的結果就是不可預料的,執行緒是不安全的。

        因此,執行緒安全,一定是對多執行緒而言的;單個執行緒,不存線上程安全問題。

二、多執行緒鎖

1、自旋鎖(OSSpinLock)

這要因為低優先順序的執行緒獲得鎖並訪問共享資源,這時一個高優先順序的執行緒嘗試獲得這個鎖,他會處於spin lock的忙等狀態,從而佔用大量CPU。而低優先順序執行緒無法與高優先順序執行緒爭奪CPU時間,從而導致任務遲遲無法完成,無法釋放lock。如果還想使用自旋鎖,除非開發者能保證訪問鎖的執行緒全部都處於同一優先順序,否則 iOS 系統中所有型別的自旋鎖都不能再使用了。

忙等這種自旋鎖的實現原理:

do {  
    Acquire Lock
        Critical section  // 臨界區
    Release Lock
        Reminder section // 不需要鎖保護的程式碼
}

在 Acquire Lock 這一步,我們申請加鎖,目的是為了保護臨界區(Critical Section) 中的程式碼不會被多個執行緒執行。

自旋鎖的實現思路很簡單,理論上來說只要定義一個全域性變數,用來表示鎖的可用情況即可,虛擬碼如下:


bool lock = false; /** 一開始沒有鎖上,任何執行緒都可以申請鎖   */
do {  
    while(lock); /** 如果 lock 為 true 就一直死迴圈,相當於申請鎖 */ 
    lock = true; /** 掛上鎖,這樣別的執行緒就無法獲得鎖 */ 
        Critical section  /** 臨界區 */ 
    lock = false; /** 相當於釋放鎖,這樣別的執行緒可以進入臨界區 */
        Reminder section /** 不需要鎖保護的程式碼  */        
}

初始化lock的全域性變數,一開始是false,while(lock)的意思是,當lock為true的時候,就進行忙等死迴圈(do-while申請鎖),由於一開始是false,直接退出迴圈,然後lock鎖上,執行臨界區程式碼,也就是這個時候有其他執行緒訪問,lock已經被鎖上,while迴圈會一直忙等,處於申請鎖狀態,上一個鎖執行完任務,就會解鎖,這個時候lock變成了false,之前其他執行緒忙等狀態下的條件變了,跳出迴圈,下一個執行緒執行lock=true,進門執行任務,其他執行緒繼續等待。

為什麼忙等會導致低優先順序執行緒拿不到時間片?這還得從操作系統的執行緒排程說起。

現代作業系統在管理普通執行緒時,通常採用時間片輪轉演算法(Round Robin,簡稱 RR)。每個執行緒會被分配一段時間片(quantum),通常在 10-100 毫秒左右。當執行緒用完屬於自己的時間片以後,就會被作業系統掛起,放入等待佇列中,直到下一次被分配時間片。

    OSSpinLock lock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&lock);
    /** 需要執行的程式碼 */
    OSSpinLockUnlock(&lock);
   /** OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock)蘋果在OSSpinLock註釋表示被廢棄,改用os_unfair_lock鎖替代 */

2.訊號量(dispatch_semaphore)

訊號量的實現原理比較簡單,如果想要詳細瞭解,可以參考我這篇文章GCD詳解

dispatch_semaphore_t lock = dispatch_semaphore_create(1);/** 傳入的引數必須大於或者等於0,否則會返回Null,如果想要當作執行緒鎖使用,則必須設定訊號量為1 */
long wait = dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); /** 訊號量減1操作 */
/** 需要執行的程式碼 */
long signal = dispatch_semaphore_signal(lock);/** 訊號量進行加1處理 */

⚠️dispatch_semaphore_signal與dispatch_semaphore_wait要成對出現,不然會丟擲異常

3.互斥鎖(pthread_mutex

互斥鎖的實現原理與訊號量非常相似,不是使用忙等,而是阻塞執行緒並睡眠,需要進行上下文切換。

    pthread_mutex_t lock;
    pthread_mutex_init(&lock, NULL);
    pthread_mutex_lock(&lock);
    /** 需要執行的操作 */
    pthread_mutex_unlock(&lock);

4.NSCondition

NSCondition封裝了一個互斥鎖和條件變數,它把前者的 lock 方法和後者的wait/signal 統一在 NSCondition 物件中,暴露給使用者。互斥鎖保證執行緒安全,條件變數保證執行順序。

- (void) signal {
  pthread_cond_signal(&_condition);
}
 
/** 其實這個函式是通過巨集來定義的,展開後就是這樣 */
- (void)lock {
  int err = pthread_mutex_lock(&_mutex);
}

實踐應用:

    NSCondition *condition = [[NSCondition alloc] init];
    NSMutableArray *products = [NSMutableArray array];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [condition lock];
        NSLog(@"wait for product");
        [condition wait];/** 讓當前執行緒處於等待狀態 */
        [products removeObjectAtIndex:0];
        NSLog(@"produce count reduce,總量:%zi",products.count);
        [condition unlock];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [condition lock];
        [products addObject:[[NSObject alloc] init]];
        NSLog(@"produce count add,總量:%zi",products.count);
        [condition signal];/** CPU發訊號告訴執行緒不用在等待,可以繼續執行 */
        [condition unlock];
    });

輸出結果為:

2018-10-30 16:49:57.377988+0800 執行緒鎖-18-10-30-0[6573:223427] wait for product

2018-10-30 16:49:57.378200+0800 執行緒鎖-18-10-30-0[6573:223425] produce count add,總量:1

2018-10-30 16:49:57.378345+0800 執行緒鎖-18-10-30-0[6573:223427] produce count reduce,總量:0

5.NSLock

NSLock 是 Objective-C 以物件的形式暴露給開發者的一種鎖,它的實現非常簡單,通過巨集,定義了 lock 方法:

#define    MLOCK \
- (void) lock\
{\
  int err = pthread_mutex_lock(&_mutex);\
  // 錯誤處理 ……
}

NSLock在內部封裝了一個 pthread_mutex,屬性為 PTHREAD_MUTEX_ERRORCHECK,它會損失一定效能換來錯誤提示。這裡使用巨集定義的原因是,OC 內部還有其他幾種鎖,他們的 lock 方法都是一模一樣,僅僅是內部 pthread_mutex 互斥鎖的型別不同。通過巨集定義,可以簡化方法的定義。

    NSLock *lock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        NSLog(@"執行緒1");
        sleep(10);
        [lock unlock];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        if ([lock tryLock]) {/** tryLock方法會嘗試加鎖,如果鎖不可用(已經被鎖住),剛並不會阻塞執行緒,並返回NO */
            NSLog(@"執行緒2");
            [lock unlock];
        } else {
            NSLog(@"嘗試加鎖失敗");
        }
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        if ([lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]) {/** lockBeforeDate:方法會在所指定Date之前嘗試加鎖,如果在指定時間之前都不能加鎖,則返回NO。 */
            NSLog(@"執行緒3");
            [lock unlock];
        } else {
            NSLog(@"嘗試加鎖失敗");
        }
    });

輸出結果為:

2018-10-30 17:14:25.464806+0800 執行緒鎖-18-10-30-0[6797:234890] 執行緒1

2018-10-30 17:14:26.465822+0800 執行緒鎖-18-10-30-0[6797:234891] 嘗試加鎖失敗

2018-10-30 17:14:35.469674+0800 執行緒鎖-18-10-30-0[6797:234892] 執行緒3

6.pthread_mutex(recursive)

pthread_mutex(recursive)是為了防止在遞迴的情況下出現死鎖而出現的遞迴鎖。作用和NSRecursiveLock遞迴鎖類似。

    pthread_mutex_t lock;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    /**
      * PTHREAD_MUTEX_NORMAL 互斥鎖不會檢測死鎖
      * PTHREAD_MUTEX_ERRORCHECK 互斥鎖可提供錯誤檢查
      * PTHREAD_MUTEX_RECURSIVE 遞迴鎖
      * PTHREAD_MUTEX_DEFAULT 對映到 PTHREAD_PROCESS_NORMAL
      */
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&lock, &attr);
    pthread_mutexattr_destroy(&attr);
    pthread_mutex_lock(&lock);
    /** 需要執行的程式碼 */
    pthread_mutex_unlock(&lock);

由於 pthread_mutex 有多種型別,可以支援遞迴鎖等,因此在申請加鎖時,需要對鎖的型別加以判斷,這也就是為什麼它和訊號量的實現類似,但效率略低的原因。

7.遞迴鎖(NSRecursiveLock)

NSRecursiveLock實際上定義的是一個遞迴鎖,這個鎖可以被同一執行緒多次請求,而不會引起死鎖。這主要是用在迴圈或遞迴操作中。

遞迴鎖也是通過 pthread_mutex_lock 函式來實現,在函式內部會判斷鎖的型別。NSRecursiveLock 與 NSLock 的區別在於內部封裝的 pthread_mutex_t 物件的型別不同,前者的型別為 PTHREAD_MUTEX_RECURSIVE。

    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^RecursiveMethod)(int);
        RecursiveMethod = ^(int value) {
            [lock lock];
            if (value > 0) {
                NSLog(@"value = %d", value);
                RecursiveMethod(value - 1);
            }
            [lock unlock];
        };
        RecursiveMethod(5);
    });

輸出結果為:

2018-10-30 17:52:24.876873+0800 執行緒鎖-18-10-30-0[7087:251693] value = 5

2018-10-30 17:52:24.877148+0800 執行緒鎖-18-10-30-0[7087:251693] value = 4

2018-10-30 17:52:24.877207+0800 執行緒鎖-18-10-30-0[7087:251693] value = 3

2018-10-30 17:52:24.877254+0800 執行緒鎖-18-10-30-0[7087:251693] value = 2

2018-10-30 17:52:24.877296+0800 執行緒鎖-18-10-30-0[7087:251693] value = 1

8.條件鎖(NSConditionLock)

NSConditionLock 藉助 NSCondition 來實現,它的本質就是一個生產者-消費者模型。“條件被滿足”可以理解為生產者提供了新的內容。NSConditionLock 的內部持有一個 NSCondition 物件,以及 _condition_value 屬性,在初始化時就會對這個屬性進行賦值:

- (id) initWithCondition:(NSInteger)value {
    if (nil != (self = [super init])) {
        _condition = [NSCondition new]
        _condition_value = value;
    }
    return self;
}

它的 lockWhenCondition 方法其實就是消費者方法:

- (void)lockWhenCondition: (NSInteger)value {
    [_condition lock];
    while (value != _condition_value) {
        [_condition wait];
    }
}

對應的 unlockWhenCondition 方法則是生產者,使用了 broadcast 方法通知了所有的消費者:

- (void)unlockWithCondition: (NSInteger)value {
    _condition_value = value;
    [_condition broadcast];
    [_condition unlock];
}
 NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lockWhenCondition:1];
        NSLog(@"執行緒1");
        sleep(2);
        [lock unlock];
    });
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        if ([lock tryLockWhenCondition:0]) {/** 只有condition為0,才可以進行枷鎖 */
            NSLog(@"執行緒2");
            [lock unlockWithCondition:2];/** 解鎖,並設定condition為2 */
            NSLog(@"執行緒2解鎖成功");
        } else {
            NSLog(@"執行緒2嘗試加鎖失敗");
        }
    });
    
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(2);/** 以保證讓執行緒2的程式碼先執行執行 */
        if ([lock tryLockWhenCondition:2]) {
            NSLog(@"執行緒3");
            [lock unlock];
            NSLog(@"執行緒3解鎖成功");
        } else {
            NSLog(@"執行緒3嘗試加鎖失敗");
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(3);/** 以保證讓執行緒2的程式碼先執行執行 */
        if ([lock tryLockWhenCondition:2]) {
            NSLog(@"執行緒4");
            [lock unlockWithCondition:1];
            NSLog(@"執行緒4解鎖成功");
        } else {
            NSLog(@"執行緒4嘗試加鎖失敗");
        }
    });

輸出結果為:

2018-10-31 10:10:34.242631+0800 執行緒鎖-18-10-30-0[3120:44938] 執行緒2

2018-10-31 10:10:34.242815+0800 執行緒鎖-18-10-30-0[3120:44938] 執行緒2解鎖成功

2018-10-31 10:10:35.242689+0800 執行緒鎖-18-10-30-0[3120:45290] 執行緒3

2018-10-31 10:10:35.242864+0800 執行緒鎖-18-10-30-0[3120:45290] 執行緒3解鎖成功

2018-10-31 10:10:36.241514+0800 執行緒鎖-18-10-30-0[3120:45291] 執行緒4

2018-10-31 10:10:36.241726+0800 執行緒鎖-18-10-30-0[3120:45291] 執行緒4解鎖成功

2018-10-31 10:10:36.241742+0800 執行緒鎖-18-10-30-0[3120:45289] 執行緒1

[email protected]

@synchronized是一個 OC 層面的鎖, 通過犧牲效能換來語法上的簡潔與可讀。這是通過一個雜湊表來實現的,OC 在底層使用了一個互斥鎖的陣列(你可以理解為鎖池),通過對物件去雜湊值來得到對應的互斥鎖。

@synchronized(/** 需要鎖住的物件 */) {
     /** 需要加鎖的程式碼 */
}

10.os_unfair_lock

os_unfair_lock iOS 10.0新推出的鎖,用於解決OSSpinLock優先順序反轉問題。

    /** 初始化 */ 
    os_unfair_lock_t unfairLock = &(OS_UNFAIR_LOCK_INIT);
    /** 加鎖 */ 
    os_unfair_lock_lock(unfairLock);
    /** 解鎖 */ 
    os_unfair_lock_unlock(unfairLock);
    /** 嘗試加鎖 */
    BOOL b = os_unfair_lock_trylock(unfairLock);

執行緒鎖效能對比

ibireme 不再安全的 OSSpinLock的 一文中,有一張圖片簡單的比較了各種鎖的加解鎖效能:

在文中也給出了測試程式碼,本人根據YY大神的提供的程式碼進行了最新的執行緒鎖效能模擬測試:

OSSpinLock:                   0.03 ms
dispatch_semaphore:           0.02 ms
pthread_mutex:                0.03 ms
NSCondition:                  0.02 ms
NSLock:                       0.03 ms
pthread_mutex(recursive):     0.03 ms
NSRecursiveLock:              0.04 ms
NSConditionLock:              0.08 ms
@synchronized:                0.12 ms
os_unfair_lock:               0.03 ms
---- fin (1000) ----
OSSpinLock:                   0.80 ms
dispatch_semaphore:           1.48 ms
pthread_mutex:                2.21 ms
NSCondition:                  2.55 ms
NSLock:                       2.66 ms
pthread_mutex(recursive):     3.35 ms
NSRecursiveLock:              4.40 ms
NSConditionLock:              7.04 ms
@synchronized:               10.88 ms
os_unfair_lock:               1.47 ms
---- fin (100000) ----
OSSpinLock:                  77.82 ms
dispatch_semaphore:         141.42 ms
pthread_mutex:              216.28 ms
NSCondition:                228.89 ms
NSLock:                     241.07 ms
pthread_mutex(recursive):   343.69 ms
NSRecursiveLock:            434.11 ms
NSConditionLock:            690.40 ms
@synchronized:             1034.34 ms
os_unfair_lock:             141.99 ms
---- fin (10000000) ----

從中不難看出OSSPinLock,dispatch_semaphore的效能遠遠優於其他的鎖,但是OSSpinLock由於優先順序反轉的問題,蘋果在iOS 10的時候推出了os_unfair_lock來替代,而且效能不減當年,但是要在iOS 10之後才能用(雖然自旋鎖的效能優於互斥鎖),而我們最常用的@synchronize明顯效能最差,如果專案中對效能特別敏感,建議使用dispatch_semaphore,如果基於方便的話就用@synchronize就可以了。