iOS的線程安全與鎖

Cat.png

在iOS編碼中,鎖的出現其實是因為多線程會出現線程安全的問題。那么,問題來了,什么是線程安全?為什么鎖可以解決線程安全問題?單線程是不是絕對的線程安全?iOS編程有多少種鎖?加解鎖的效率如何?......

一、什么是線程安全?

WIKI: Thread-safe code only manipulates shared data structures in a manner that ensures that all threads behave properly and fulfil their design specifications without unintended interaction.

用人話來說:多線程操作共享數據不會出現想不到的結果就是線程安全的,否則,是線程不安全的。

舉個例子:

NSInteger total = 0;
- (void)threadNotSafe {
    for (NSInteger index = 0; index < 3; index++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            total += 1;
            NSLog(@"total: %ld", total);
            total -= 1;
            NSLog(@"total: %ld", total);
        });
    }
}

//第一次輸出:
2017-11-28 23:34:11.551570+0800 BasicDemo[75679:5312246] total: 1
2017-11-28 23:34:11.551619+0800 BasicDemo[75679:5312248] total: 3
2017-11-28 23:34:11.551618+0800 BasicDemo[75679:5312249] total: 2
2017-11-28 23:34:11.552120+0800 BasicDemo[75679:5312246] total: 2
2017-11-28 23:34:11.552143+0800 BasicDemo[75679:5312248] total: 1
2017-11-28 23:34:11.552171+0800 BasicDemo[75679:5312249] total: 0

//第二次輸出
2017-11-28 23:34:55.738947+0800 BasicDemo[75683:5313401] total: 1
2017-11-28 23:34:55.738979+0800 BasicDemo[75683:5313403] total: 2
2017-11-28 23:34:55.738985+0800 BasicDemo[75683:5313402] total: 3
2017-11-28 23:34:55.739565+0800 BasicDemo[75683:5313401] total: 2
2017-11-28 23:34:55.739570+0800 BasicDemo[75683:5313402] total: 1
2017-11-28 23:34:55.739577+0800 BasicDemo[75683:5313403] total: 0

NSInteger total = 0;
NSLock *lock = [NSLock new];
- (void)threadSafe {
    for (NSInteger index = 0; index < 3; index++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            total += 1;
            NSLog(@"total: %ld", total);
            total -= 1;
            NSLog(@"total: %ld", total);
            [lock unlock];
        });
    }
}

//第一次輸出
2017-11-28 23:35:37.696614+0800 BasicDemo[75696:5314483] total: 1
2017-11-28 23:35:37.696928+0800 BasicDemo[75696:5314483] total: 0
2017-11-28 23:35:37.696971+0800 BasicDemo[75696:5314481] total: 1
2017-11-28 23:35:37.696995+0800 BasicDemo[75696:5314481] total: 0
2017-11-28 23:35:37.697026+0800 BasicDemo[75696:5314482] total: 1
2017-11-28 23:35:37.697050+0800 BasicDemo[75696:5314482] total: 0

//第二次輸出
2017-11-28 23:36:01.790264+0800 BasicDemo[75700:5315159] total: 1
2017-11-28 23:36:01.790617+0800 BasicDemo[75700:5315159] total: 0
2017-11-28 23:36:01.790668+0800 BasicDemo[75700:5315161] total: 1
2017-11-28 23:36:01.790687+0800 BasicDemo[75700:5315161] total: 0
2017-11-28 23:36:01.790711+0800 BasicDemo[75700:5315160] total: 1
2017-11-28 23:36:01.790735+0800 BasicDemo[75700:5315160] total: 0

第一個函數第一次和第二次調用的結果不一樣,換句話說,不能確定代碼的運行順序和結果,是線程不安全的;第二個函數第一次和第二次輸出結果一樣,可以確定函數的執行結果,是線程安全的。
居于線程安全的含義,知道線程安全是相對于多線程而言的,單線程不會存在線程安全問題。因為,單線程代碼的執行順序是確定的,可以知道代碼的執行結果。

二、鎖鎖鎖??

Lock.png

線程不安全是由于多線程訪問造成的,那么如何解決?
1.既然線程安全問題是由多線程引起的,那么,最極端的可以使用單線程保證線程安全。
2.線程安全是由于多線程訪問和修改共享資源而引起不可預測的結果,因此,如果都是訪問共享資源而不去修改共享資源也可以保證線程安全,比如:設置只讀屬性的全局變量。
3.使用鎖。

引用 ibireme《不再安全的 OSSpinLock》中的一張圖片說明加解鎖的效率:

Locks.png

我也下載了 ibiremeGitHub 上面的Demo來跑過(環境 iPhone6 iOS11.1)。發現,不同的循環次數,結果都不一樣,并沒有得到和 ibireme 一樣的結果。所以,上面的柱狀圖也只做一個定向分析,并不是很準確的結果。

下面會對這些鎖的實現原理和用法做簡單的總結和處理(詳細的實現原理,可以看 bestswift 的這篇文章《深入理解 iOS 開發中的鎖》):

OSSpinLock:

自旋鎖的實現原理比較簡單,就是死循環。當a線程獲得鎖以后,b線程想要獲取鎖就需要等待a線程釋放鎖。在沒有獲得鎖的期間,b線程會一直處于忙等的狀態。如果a線程在臨界區的執行時間過長,則b線程會消耗大量的cpu時間,不太劃算。所以,自旋鎖用在臨界區執行時間比較短的環境性能會很高。

自旋鎖的代碼實現:

#import <libkern/OSAtomic.h>

OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
//需要執行的代碼
OSSpinLockUnlock(&lock);

//OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock)
//蘋果在OSSpinLock注釋表示被廢棄,改用不安全的鎖替代
dispatch_semaphore:

dispatch_semaphore實現的原理和自旋鎖有點不一樣。首先會先將信號量減一,并判斷是否大于等于0,如果是,則返回0,并繼續執行后續代碼,否則,使線程進入睡眠狀態,讓出cpu時間。直到信號量大于0或者超時,則線程會被重新喚醒執行后續操作。

dispatch_semaphore_t lock = dispatch_semaphore_create(1);    //傳入的參數必須大于或者等于0,否則會返回Null
long wait = dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);    //wait = 0,則表示不需要等待,直接執行后續代碼;wait != 0,則表示需要等待信號或者超時,才能繼續執行后續代碼。lock信號量減一,判斷是否大于0,如果大于0則繼續執行后續代碼;lock信號量減一少于或者等于0,則等待信號量或者超時。
//需要執行的代碼
long signal = dispatch_semaphore_signal(lock);    //signal = 0,則表示沒有線程需要其處理的信號量,換句話說,沒有需要喚醒的線程;signal != 0,則表示有一個或者多個線程需要喚醒,則喚醒一個線程。(如果線程有優先級,則喚醒優先級最高的線程,否則,隨機喚醒一個線程。)
pthread_mutex:

pthread_mutex表示互斥鎖,和信號量的實現原理類似,也是阻塞線程并進入睡眠,需要進行上下文切換。

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    
pthread_mutex_t lock;
pthread_mutex_init(&lock, &attr);    //設置屬性
    
pthread_mutex_lock(&lock);    //上鎖
//需要執行的代碼
pthread_mutex_unlock(&lock);    //解鎖
NSLock:

NSLock在內部封裝了一個 pthread_mutex,屬性為 PTHREAD_MUTEX_ERRORCHECK

NSLock *lock = [NSLock new];
[lock lock];
//需要執行的代碼
[lock unlock];
NSCondition:

NSCondition封裝了一個互斥鎖和條件變量。互斥鎖保證線程安全,條件變量保證執行順序。

NSCondition *lock = [NSCondition new];
[lock lock];
//需要執行的代碼
[lock unlock];
pthread_mutex(recursive):

pthread_mutex鎖的一種,屬于遞歸鎖。一般一個線程只能申請一把鎖,但是,如果是遞歸鎖,則可以申請很多把鎖,只要上鎖和解鎖的操作數量就不會報錯。

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    
pthread_mutex_t lock;
pthread_mutex_init(&lock, &attr);    //設置屬性
    
pthread_mutex_lock(&lock);    //上鎖
//需要執行的代碼
pthread_mutex_unlock(&lock);    //解鎖
NSRecursiveLock:

遞歸鎖,pthread_mutex(recursive)的封裝。

NSRecursiveLock *lock = [NSRecursiveLock new];
[lock lock];
//需要執行的代碼
[lock unlock];
NSConditionLock:

NSConditionLock借助 NSCondition 來實現,本質是生產者-消費者模型。

NSConditionLock *lock = [NSConditionLock new];
[lock lock];
//需要執行的代碼
[lock unlock];
@synchronized:

一個對象層面的鎖,鎖住了整個對象,底層使用了互斥遞歸鎖來實現。

NSObject *object = [NSObject new];
@synchronized(object) {
  //需要執行的代碼
}

三、總結

這里只是一些簡單的總結,更多深入的研究請自行 Google

參考:

深入理解 iOS 開發中的鎖
不再安全的 OSSpinLock
Threading Programming Guide
關于 @synchronized,這兒比你想知道的還要多
OS中保證線程安全的幾種方式與性能對比
iOS 常見知識點(三):Lock
iOS多線程到底不安全在哪里?

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容