iOS多執行緒之執行緒安全

weixin_33670713發表於2017-08-29

多執行緒的使用提升了程式的效能從而提升了使用者體驗,但是同時也有一定的風險,那就是多個執行緒同時修改某一個資源造成的資源狀態不確定性問題,可能我們希望在執行某個操作的過程中,只允許該操作對某個資源進行訪問,所以同步工具就應運而生了。

Atomic Operations

原子操作是一種簡單的同步形式,它適用於簡單的資料型別。原子操作的優點是它們不阻塞競爭執行緒。簡單的操作,如增加一個計數器變數,這可比走鎖導致更好的效能。

Memory Barriers and Volatile Variables

為了達到最佳的效能,編譯器經常對彙編級指令進行重新排序,使處理器的指令管道盡可能的完整。作為這種優化的一部分,編譯器可能重新排序當它認為這樣做不會產生錯誤資料時訪問主存的指令。不幸的是,編譯器不可能總是檢測到所有與記憶體相關的操作。如果看似分離的變數實際上相互影響,編譯器優化可以以錯誤的順序更新這些變數,從而產生潛在的不正確結果。
記憶體阻塞是一種非阻塞同步工具,用於確保記憶體操作發生在正確的順序。記憶體阻塞就像一個柵欄,迫使處理器在允許執行載入和儲存操作之前完成位於該柵欄前面的所有載入和儲存操作。記憶體阻塞通常用於確保一個執行緒(但另一個執行緒可見)的記憶體操作總是以預期的順序出現。在這種情況下缺乏記憶體阻塞可能會讓其他執行緒看到看似不可能的結果。(例如,記憶體阻塞)使用記憶體阻塞,你只需呼叫OSMemoryBarrier函式在程式碼中相應的功能。
Volatile變數將另一種型別的記憶體約束應用於單個變數。編譯器通常通過將變數的值載入到暫存器中來優化程式碼。對於區域性變數,這通常不是問題。但是,如果該變數從另一個執行緒可見,那麼這種優化可能會阻止其他執行緒注意到它的任何更改。將Volatile 關鍵字應用於變數,迫使編譯器每次使用該變數時從記憶體中載入該變數。如果一個變數的值可以由編譯器無法檢測的外部源隨時更改,則可以宣告該變數是不穩定的。

Perform Selector Routines

Cocoa applications有一種方便的方式,以同步的方式將訊息傳遞給單個執行緒。NSObject類宣告的方法之一,應用程式的活動執行緒執行一個selector。這些方法讓您的執行緒非同步傳遞訊息,並保證它們將由目標執行緒同步執行。例如,您可以使用執行選擇器訊息將分散式計算的結果傳遞給應用程式的主執行緒或指定的協調執行緒。執行選擇器的每個請求都在目標執行緒的執行迴圈上排隊,然後按照收到的順序依次處理這些請求。
有關執行選擇器例程的摘要和有關如何使用它們的更多資訊,請參考Cocoa Perform Selector Sources

Using Locks

鎖是執行緒程式設計的基本同步工具。鎖使您能夠輕鬆地保護大部分程式碼,這樣您就可以確保程式碼的正確性。OS X和IOS為所有應用程式型別提供基本的互斥鎖,基礎框架為特殊情況定義了一些互斥鎖的變體。下面的部分將向您展示如何使用這些鎖型別。

  • @synchronized

這個方法應該是大家最常用的一種方式,不需要我們直接建立鎖,建立鎖的過程被系統封裝起來了,所以是開發成本最低的一種方式,但很不幸,這種方法是效能最差的,而且如果我們傳入的物件是nil,那麼它什麼用都沒有,不是執行緒安全的。具體原因可以看這裡
看一下@synchronized的用法和作用

- (void)synchronizedLock {
    __block NSMutableArray *array = [[NSMutableArray alloc]initWithObjects:@"hello", @"world", nil];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @synchronized(array) {
            sleep(2);
            NSLog(@"1-----%@", [NSThread currentThread]);
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @synchronized(array) {
            sleep(1);
            NSLog(@"2-----%@", [NSThread currentThread]);
        }
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [array addObject:@"zz"];
        @synchronized (array) {
            sleep(1);
            NSLog(@"3-----%@", [NSThread currentThread]);
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSMutableArray *array1 = [array copy];
        @synchronized (array1) {
            sleep(1);
            NSLog(@"4----%@", [NSThread currentThread]);
        }
    });
}

一起看下輸出結果:

輸出結果:
2017-08-29 09:26:57.956 Lock[4367:485949] 4----<NSThread: 0x600000078880>{number = 3, name = (null)}
2017-08-29 09:26:58.954 Lock[4367:485929] 1-----<NSThread: 0x600000265180>{number = 4, name = (null)}
2017-08-29 09:26:59.960 Lock[4367:485932] 2-----<NSThread: 0x600000262d40>{number = 5, name = (null)}
2017-08-29 09:27:00.965 Lock[4367:485930] 3-----<NSThread: 0x608000266700>{number = 6, name = (null)}

我們程式碼中讓1沉睡2秒,4沉睡1秒,從輸出結果看兩個任務是同時啟動,所以4和其他不是互斥鎖,而1、2、3明顯就是在等待前一個任務完成才開始,所以是互斥鎖,而且在3中我們是向陣列中加入新元素的,因而我們得出結論,@synchronized不是根據物件的內容來的,看過分析的應該知道,@synchronized鎖是根據地址來的。

  • POSIX Mutex Lock

POSIX互斥鎖的使用在任何應用程式都非常容易。建立互斥鎖,你宣告和初始化一個pthread_mutex_t結構。鎖定和解鎖的互斥鎖,你用pthread_mutex_lock和pthread_mutex_unlock功能。

- (void)posixMutex {
    __block pthread_mutex_t mutex; //宣告一個pthread_mutex_t結構體
    pthread_mutex_init(&mutex, NULL);//初始化一個pthread_mutex_t結構體
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        pthread_mutex_lock(&mutex);//加鎖
        NSLog(@"1---posix mutex---%@", [NSThread currentThread]);
        sleep(3);
        NSLog(@"2---posix mutex---%@", [NSThread currentThread]);
        pthread_mutex_unlock(&mutex);//解鎖
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        pthread_mutex_lock(&mutex);
        NSLog(@"3---posix mutex---%@", [NSThread currentThread]);
        pthread_mutex_unlock(&mutex);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        NSLog(@"4---posix mutex---%@", [NSThread currentThread]);
    });
}

輸出資訊:
2017-08-28 17:06:01.983 Lock[2250:378898] 1---posix mutex---<NSThread: 0x608000261900>{number = 3, name = (null)}
2017-08-28 17:06:02.983 Lock[2250:378880] 4---posix mutex---<NSThread: 0x600000260ec0>{number = 4, name = (null)}
2017-08-28 17:06:04.988 Lock[2250:378898] 2---posix mutex---<NSThread: 0x608000261900>{number = 3, name = (null)}
2017-08-28 17:06:04.989 Lock[2250:378899] 3---posix mutex---<NSThread: 0x60800007de00>{number = 5, name = (null)}

從輸出結果看到枷鎖的任務是同步執行,沒加鎖的就是非同步執行。

  • NSLock

NSLocking

@protocol NSLocking
- (void)lock;
- (void)unlock;
@end

NSLock是Cocoa提供給我們最基本的鎖物件,實際上所有鎖的介面(包括NSLock)是由NSLocking協議定義,它定義了鎖定和解鎖方法。您使用這些方法來獲取和釋放鎖,就像任何互斥鎖一樣。
除lock和unlock方法外,NSLock還提供了tryLock和lockBeforeDate:兩個方法,前一個方法會嘗試加鎖,如果鎖不可用(已經被鎖住),剛並不會阻塞執行緒,並返回NO。lockBeforeDate:方法會在所指定Date之前嘗試加鎖,如果在指定時間之前都不能加鎖,則返回NO。

- (void)lock {
    NSLock *lock = [[NSLock alloc] init];//初始化一個NSLock物件
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if ([lock tryLock]) {//嘗試獲取鎖,如果獲取不到返回NO,不會阻塞該執行緒
            NSLog(@"1---lock---%@", [NSThread currentThread]);
            sleep(3);
            NSLog(@"2---lock---%@", [NSThread currentThread]);
            [lock unlock];
        }else{
            NSLog(@"2---the lock is unavailable");
        }
    });
    NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:4];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if ([lock lockBeforeDate:date]) {//在date之前加鎖,如果不能返回NO
            NSLog(@"3---lock---%@", [NSThread currentThread]);
            [lock unlock];
        } else {
            NSLog(@"3---the lock is unavailable");
        }
    });
    NSDate *date2 = [[NSDate alloc] initWithTimeIntervalSinceNow:2];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if ([lock lockBeforeDate:date2]) {//在date之前加鎖,如果不能返回NO
            NSLog(@"4---lock---%@", [NSThread currentThread]);
            [lock unlock];
        } else {
            NSLog(@"4---the lock is unavailable");
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        NSLog(@"5---lock---%@", [NSThread currentThread]);
    });
}

主要看3和4是什麼情況

2017-08-28 17:49:12.104 Lock[2393:417822] 1---lock---<NSThread: 0x60800006b040>{number = 3, name = (null)}
2017-08-28 17:49:13.106 Lock[2393:417807] 5---lock---<NSThread: 0x60800006ca80>{number = 4, name = (null)}
2017-08-28 17:49:14.108 Lock[2393:417810] 4---the lock is unavailable
2017-08-28 17:49:15.108 Lock[2393:417822] 2---lock---<NSThread: 0x60800006b040>{number = 3, name = (null)}
2017-08-28 17:49:15.109 Lock[2393:417808] 3---lock---<NSThread: 0x600000068200>{number = 5, name = (null)}

5不重要,只是讓你看一下不加鎖的情況下是不會影響block的執行,因為任務是非同步排程的,所以我們能從1看到任務大致的開始時間是17:49:12,所以3是試圖在17:49:16之前加鎖,4是試圖在17:49:14之前加鎖,而1把執行緒阻塞了3秒鐘,然後其他執行緒才能獲取鎖,所以4在17:49:14的時候宣告失敗,而3在17:49:15也就是1結束的時候才獲取到鎖。

  • NSRecursiveLock

遞迴鎖,一種特殊的NSLock,同一個執行緒可以獲得多次不會導致執行緒死鎖。遞迴鎖跟蹤它成功獲取了多少次。每個成功獲取鎖必須通過相應的呼叫來解除鎖的平衡。只有當所有的鎖和解鎖呼叫都平衡時,鎖才會真正釋放,這樣其他執行緒就可以獲得它。
顧名思義,這種型別的鎖通常用於遞迴函式中,以防止遞迴阻止執行緒。您也可以在非遞迴的情況下使用它來呼叫函式,它們的語義要求它們也接受鎖。下面是一個通過遞迴獲得鎖的簡單遞迴函式的示例。當你再次掉用同樣的方法事,你沒有呼叫NSRecursiveLock鎖,會造成執行緒死鎖。

- (void)recursiveLock:(NSInteger)value {
    //初始化一個遞迴鎖
    NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
    //加鎖
    [theLock lock];
    if (value != 0) {
        NSLog(@"----%zd---", value);
        --value;
        [self recursiveLock:value];//遞迴呼叫
    }
    [theLock unlock];//解鎖
}

輸出結果:
2017-08-29 10:08:22.808 Lock[4424:515786] ----5---
2017-08-29 10:08:22.808 Lock[4424:515786] ----4---
2017-08-29 10:08:22.809 Lock[4424:515786] ----3---
2017-08-29 10:08:22.809 Lock[4424:515786] ----2---
2017-08-29 10:08:22.809 Lock[4424:515786] ----1---

注意:因為在所有的鎖呼叫與解鎖呼叫相平衡之前不會釋放一個遞迴鎖,因此您應該仔細考慮鎖對效率的影響,在遞迴完成之前的這段內持有任何鎖會導致其他執行緒阻塞,如果您可以重寫程式碼以消除遞迴或消除使用遞迴鎖的必要性,則可以獲得更好的效能。

  • NSConditionLock

NSConditionLock定義了一個可以鎖定和解鎖互斥鎖的具體值。您不應該將這種型別的鎖與條件(下邊介紹)混淆。行為與條件有些相似,但實現的方式非常不同。
NSConditionLock通常使用線上程需要在一個特定的順序執行任務,例如當一個執行緒產生的資料,另一個執行緒消耗資料。當生產者正在執行時,消費者使用特定於程式的條件獲取鎖。當生產者完成時(條件本身是一個整數值,您定義),它開啟鎖並將鎖定條件設定為適當的整數值來喚醒消費執行緒,然後處理資料。
NSConditionLock物件能夠響應加鎖解鎖的任意組合。例如,你可以用 lock 和 unlockWithCondition: 配對,或lockwhencondition:和 unlock 配對。當然,後一種組合方式可能不會釋放等待特定條件值的執行緒。
下面的示例演示如何使用條件鎖處理生產者消費者問題。設想一個應用程式包含一個資料佇列。生產者執行緒將資料新增到佇列中,而消費執行緒從佇列中提取資料。生產商不必等待特定的條件,但必須等待鎖可用,以便安全地向佇列中新增資料。

- (void)conditionLock {
    NSMutableArray *products = [NSMutableArray array];
    NSInteger HAS_DATA = 1;//條件常數
    NSInteger NO_DATA = 0;//條件常數
    id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (YES) {
            [condLock lockWhenCondition:NO_DATA];
            [products addObject:[[NSObject alloc] init]];
            NSLog(@"product---%zi",products.count);
            sleep(1);
            [condLock unlockWithCondition:HAS_DATA];
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (YES) {
            NSLog(@"wait for product");
            [condLock lockWhenCondition:HAS_DATA];
            [products removeObjectAtIndex:0];
            NSLog(@"product---%zi",products.count);
            BOOL isEmpty = products.count;
            [condLock unlockWithCondition:(!isEmpty ? NO_DATA : HAS_DATA)];
        }
    });
}

看輸出:

2017-08-29 11:58:59.977 Lock[4565:597516] wait for product
2017-08-29 11:58:59.977 Lock[4565:597517] product---1
2017-08-29 11:59:00.981 Lock[4565:597516] product---0
2017-08-29 11:59:00.982 Lock[4565:597516] wait for product
2017-08-29 11:59:00.982 Lock[4565:597517] product---1
2017-08-29 11:59:01.982 Lock[4565:597516] product---0
2017-08-29 11:59:01.983 Lock[4565:597516] wait for product
2017-08-29 11:59:01.983 Lock[4565:597517] product---1
2017-08-29 11:59:02.986 Lock[4565:597516] product---0

初始化的時候條件為0,所以執行緒1順利生產了一個產品,產品2想要獲取同一個鎖必須等到條件達到之後才可以,只能等待執行緒1解鎖,之後2消耗一個產品,這之後1也要獲取同一個鎖也和之前2一樣,需要條件達到。

Conditions

Conditions是一種特殊型別的鎖,你可以使用它來同步操作必須執行的順序。它們互斥的方式與互斥鎖不同。等待某個條件的執行緒會被阻塞一直到該條件由另一個執行緒顯式地發出訊號為止。
由於實現作業系統所涉及到的細微之處,即使它們實際上沒有由你的程式碼發出訊號條件鎖也返回成功。為了避免由這些虛假訊號引起的問題,你應該始終使用謂詞與條件鎖定相結合。謂詞是確定執行緒是否安全進行的一種更具體的方法。該條件只會使執行緒處於睡眠狀態,直到謂詞可以由訊號執行緒設定為止。

  • NSCondition

NSCondition和POSIX conditions提供相同的語義,除了在單個物件中包裝必需的鎖和條件資料結構。結果是一個物件可以像互斥鎖那樣鎖定,然後像條件一樣等待,但是這些等待和發出訊號都是需要我們手動控制。
簡單用法

- (void)condition {
    NSCondition *condition = [[NSCondition alloc] init];
    NSMutableArray *products = [NSMutableArray array];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (YES) {
            [condition lock];
            if (!products.count) {
                NSLog(@"wait for product");
                [condition wait];//讓執行緒處於等待
            }
            [products removeObjectAtIndex:0];
            NSLog(@"product---%zi",products.count);
            [condition unlock];
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (YES) {
            [condition lock];
            [products addObject:[[NSObject alloc] init]];
            NSLog(@"product---%zi",products.count);
            [condition signal];//發出訊號讓等待的執行緒繼續執行
            [condition unlock];
            sleep(1);
        }
    });
}

看輸出結果:

2017-08-29 14:32:36.149 Lock[5501:680404] wait for product
2017-08-29 14:32:36.150 Lock[5501:680419] product---1
2017-08-29 14:32:36.150 Lock[5501:680404] product---0
2017-08-29 14:32:36.150 Lock[5501:680404] wait for product
2017-08-29 14:32:37.155 Lock[5501:680419] product---1
2017-08-29 14:32:37.156 Lock[5501:680404] product---0
2017-08-29 14:32:37.156 Lock[5501:680404] wait for product
2017-08-29 14:32:38.156 Lock[5501:680419] product---1
2017-08-29 14:32:38.156 Lock[5501:680404] product---0

產品數為0的時候沒辦法消耗,所以只能等待,線上程2中生產完產品之後發出一個訊號告訴執行緒1可以繼續執行,執行緒1消耗一個產品,然後接著依次迴圈。

  • POSIX Conditions

POSIX執行緒狀態鎖需要條件的資料結構和互斥使用一起使用。雖然這兩個鎖結構是分開的,但是互斥鎖與執行時的條件結構緊密相連。等待訊號的執行緒應該總是使用相同的互斥鎖和條件結構。更改配對會導致錯誤。
經過初始化的條件與互斥鎖,等待執行緒進入使用ready_to_go變數做謂詞。只有當謂詞被設定並且條件隨後發出訊號時,等待執行緒才能喚醒並開始工作。

- (void)posixCondition {
    __block pthread_mutex_t mutex;
    __block pthread_cond_t condition;
    __block Boolean     ready_to_go = false;
    NSMutableArray *products = [NSMutableArray array];
    pthread_mutex_init(&mutex, NULL);//初始化執行緒互斥鎖
    pthread_cond_init(&condition, NULL);//初始化條件
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (YES) {
            // Lock the mutex.
            pthread_mutex_lock(&mutex);
            // If the predicate is already set, then the while loop is bypassed;
            // otherwise, the thread sleeps until the predicate is set.
            while(ready_to_go == false) {
                NSLog(@"wait for product");
                pthread_cond_wait(&condition, &mutex);
            }
            // Do work. (The mutex should stay locked.)
            [products removeObjectAtIndex:0];
            NSLog(@"product---%zi",products.count);
            // Reset the predicate and release the mutex.
            ready_to_go = false;
            pthread_mutex_unlock(&mutex);
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (YES) {
            // At this point, there should be work for the other thread to do.
            pthread_mutex_lock(&mutex);
            [products addObject:[[NSObject alloc] init]];
            NSLog(@"product---%zi",products.count);
            ready_to_go = true;
            // Signal the other thread to begin work.
            pthread_cond_signal(&condition);
            pthread_mutex_unlock(&mutex);
            sleep(1);
        }
    });
}

看下輸出結果

2017-08-29 15:05:28.247 Lock[5559:708783] wait for product
2017-08-29 15:05:28.248 Lock[5559:708784] product---1
2017-08-29 15:05:28.248 Lock[5559:708783] product---0
2017-08-29 15:05:28.248 Lock[5559:708783] wait for product
2017-08-29 15:05:29.250 Lock[5559:708784] product---1
2017-08-29 15:05:29.250 Lock[5559:708783] product---0
2017-08-29 15:05:29.251 Lock[5559:708783] wait for product
2017-08-29 15:05:30.255 Lock[5559:708784] product---1
2017-08-29 15:05:30.256 Lock[5559:708783] product---0

和上面一樣,不過換了執行緒鎖,並且加了謂詞,效果是一樣的。

dispatch_semaphore

訊號量我們在講GCD的時候已經講過了,還裡還是再說一遍,加深一下印象

訊號量(semaphore)是非負整型變數,除了初始化之外,它只能通過兩個標準原子操作:wait(semap) , signal(semap) ; 來進行訪問;
操作也被成為PV原語(P來源於Dutch proberen"測試",V來源於Dutch verhogen"增加"),而普通整型變數則可以在任何語句塊中被訪問;

在GCD中有semaphore的操作:
dispatch_semaphore_create 建立一個semaphore 
dispatch_semaphore_signal 傳送一個訊號 
dispatch_semaphore_wait 等待訊號

訊號量的主要作用就是控制併發量,訊號量為0則阻塞執行緒,大於0則不會阻塞。我們通過改變訊號量的值,來控制是否阻塞執行緒,從而控制併發控制。
來看個例子

- (void)singal {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);//建立一個值為1的訊號量
    NSLog(@"singal ----%zd", index);
    for (int index = 0; index < 5; index++) {
        dispatch_async(queue, ^(){
            NSLog(@"singal ----%zd", index);
            [NSThread sleepForTimeInterval:1];
            dispatch_semaphore_signal(semaphore);//訊號量+1
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//試圖把訊號量-1,如果值小於0,就等待
    }
}

看輸出結果:
2017-08-24 17:58:42.057 thread[1326:93346] singal ----1
2017-08-24 17:58:42.057 thread[1326:93349] singal ----0
2017-08-24 17:58:43.062 thread[1326:93346] singal ----2
2017-08-24 17:58:43.063 thread[1326:93349] singal ----3
2017-08-24 17:58:44.065 thread[1326:93349] singal ----4

因為我們建立的訊號量初始值為1,然後我們是先執行新增任務道佇列,然後-1,所以我們就有兩個併發。如果我們建立的訊號量為0,或者我們先-1,然後新增任務道佇列,那我們就只有一個併發,就等於是同步操作。

相關文章