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就可以了。