在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
第一個函數第一次和第二次調用的結果不一樣,換句話說,不能確定代碼的運行順序和結果,是線程不安全的;第二個函數第一次和第二次輸出結果一樣,可以確定函數的執行結果,是線程安全的。
居于線程安全的含義,知道線程安全是相對于多線程而言的,單線程不會存在線程安全問題。因為,單線程代碼的執行順序是確定的,可以知道代碼的執行結果。
二、鎖鎖鎖??
線程不安全是由于多線程訪問造成的,那么如何解決?
1.既然線程安全問題是由多線程引起的,那么,最極端的可以使用單線程保證線程安全。
2.線程安全是由于多線程訪問和修改共享資源而引起不可預測的結果,因此,如果都是訪問共享資源而不去修改共享資源也可以保證線程安全,比如:設置只讀屬性的全局變量。
3.使用鎖。
引用 ibireme 在《不再安全的 OSSpinLock》中的一張圖片說明加解鎖的效率:
我也下載了 ibireme 在 GitHub 上面的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多線程到底不安全在哪里?