小記
在IOS上進行多執行緒開發,為了保證執行緒安全,防止資源競爭,需要給程式進行加鎖,通常用到的程式鎖分為7種。
- 訊號量
- 互斥鎖
- 自旋鎖
- 遞迴鎖
- 條件鎖
- 讀寫鎖
- 分散式鎖
鎖
鎖:是保證執行緒安全常見的同步工具,防止Data race(資料競爭)的發生。
Data race(資料競爭):
- 兩個或者更多執行緒在一個程式中,併發的訪問同一資料
- 至少一個訪問是寫入操作
- 些執行緒都不使用任何互斥鎖來控制這些訪問
pthread_mutex
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 定義鎖的屬性
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr) // 建立鎖
pthread_mutex_lock(&mutex); // 申請鎖
pthread_mutex_unlock(&mutex); // 釋放鎖
複製程式碼
其中,鎖的屬性包含一下四種:
PTHREAD_MUTEX_NORMAL:預設值普通鎖,當一個執行緒加鎖以後,其他執行緒進入按照優先順序進入等待佇列,並且解鎖的時候按照先入先出的方式獲得鎖。
PTHREAD_MUTEX_ERRORCHECK:檢錯鎖,當同一個執行緒獲得同一個鎖的時候,則返回EDEADLK,否則與普通鎖處理一樣。
PTHREAD_MUTEX_RECURSIVE:遞迴鎖。這裡有別於上面的檢錯鎖,同一個執行緒可以遞迴獲得鎖,但是加鎖和解鎖必須要一一對應。
PTHREAD_MUTEX_DEFAULT:適應鎖,等待解鎖之後重新競爭,沒有等待佇列。
複製程式碼
訊號量
dispatch_semaphore
是GCD用來同步的一種方式,dispatch_semephore_create
方法使用者建立一個dispatch_semephore_t
型別的訊號量,初始的引數必須大於0,該引數用來表示該訊號量有多少個訊號,簡單的說也就是同事允許多少個執行緒訪問。dispatch_semaphore_wait
方法是等待一個訊號量,該方法會判斷signal
的訊號值是否大於0,如果大於0則不會阻塞執行緒,消耗點一個訊號值,執行後續任務。如果訊號值等於0那麼就和NSCondition
一樣,阻塞當前執行緒進入等待狀態,如果等待時間未超過timeout並且dispatch_semaphore_signal
釋放了了一個訊號值,那麼就會消耗掉一個訊號值並且向下執行。如果期間一直不能獲得訊號量並且超過超時時間,那麼就會自動執行後續語句。
dispatch_semaphore_create(long value)
;//創造訊號量dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
;//等待訊號dispatch_semaphore_signal(dispatch_semaphore_t dsema)
;//傳送訊號
例項程式碼:
- (void)semaphoreSync {
NSLog(@"semaphore---begin");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//建立初始訊號量 為 0 ,阻塞所有執行緒
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block int number = 0;
dispatch_async(queue, ^{
// 追加任務A
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@",[NSThread currentThread]); // 列印當前執行緒
number = 100;
// 執行完執行緒,訊號量加 1,訊號總量從 0 變為 1
dispatch_semaphore_signal(semaphore);
});
//原任務B
////若計數為0則一直等待,直到接到總訊號量變為 >0 ,繼續執行後續程式碼
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end,number = %d",number);
}
複製程式碼
互斥鎖
互斥鎖的實現原理與訊號量非常相似,不是使用忙等,而是阻塞執行緒並睡眠,需要進行上下文切換。
當一個執行緒獲得這個鎖之後,其他想要獲得此鎖的執行緒將會被阻塞,直到該鎖被釋放。
當臨界區加上互斥鎖以後,其他的呼叫方不能獲得鎖,只有當互斥鎖的持有方釋放鎖之後其他呼叫方才能獲得鎖。
呼叫方在獲得鎖的時候發現互斥鎖已經被其他方持有,那麼該呼叫方只能進入睡眠狀態,這樣不會佔用CPU資源。但是會有時間的消耗,系統的執行時基於CPU時間排程的,每次執行緒可能有100ms的執行時間,頻繁的CPU切換也會消耗一定的時間。
NSLock:
NSLock遵循NSLocking協議,同時也是互斥鎖,提供了lock
和unlock
方法來進行加鎖和解鎖。
NSLock內部是封裝了pthread_mutext
,型別是PTHREAD_MUTEXT_ERRORCHECK
,它會損失一定的效能換來錯誤提示。
- (void)lock;
- (void)unlock;
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
複製程式碼
tryLock
和lock
方法都會請求加鎖,唯一不同的是trylock
在沒有獲得鎖的時候可以繼續做一些任務和處理。lockBeforeDate:
方法也比較簡單,就是在limit時間點之前獲得鎖,沒有拿到鎖就返回NO。
@synchronized:
這其實是一個 OC 層面的鎖,防止不同的執行緒同時執行同一段程式碼,相比於使用 NSLock 建立鎖物件、加鎖和解鎖來說,
@synchronized
用著更方便,可讀性更高。
大體上,想要明白@synchronized
,需要知道在@synchronized
中objc_sync_enter
和objc_sync_exit
的成對呼叫,而且每個傳入的物件,都會為其分配一個遞迴鎖並儲存在雜湊表中。在objc_sync_enter
中加鎖,在objc_sync_exit
中解鎖。
具體可以參考這篇文章: 關於 @synchronized,這兒比你想知道的還要多
@synchronized(self) {
//資料操作
}
複製程式碼
自旋鎖
自旋鎖的目的是為了確保臨界區只有一個執行緒可以訪問。
當一個執行緒獲得鎖之後,其他執行緒將會一直迴圈在哪裡檢視是否該鎖被釋放,是用於多執行緒同步的一種鎖,執行緒反覆檢查鎖變數是否可用。由於執行緒在這一過程中保持執行,因此是一種忙等待。一旦獲取了自旋鎖,執行緒會一直保持該鎖,直至顯式釋放自旋鎖。自旋鎖避免了程式上下文的排程開銷,因此對於執行緒只會阻塞很短時間的場合是有效的。
由於呼叫方會一直迴圈看該自旋鎖的的保持者是否已經釋放了資源,所以總的效率來說比互斥鎖高。但是自旋鎖只用於短時間的資源訪問,如果不能短時間內獲得鎖,就會一直佔用著CPU,造成效率低下。
OSSpinLock:
自旋鎖的一種,由於在某些場景下不安全已被棄用。 需匯入標頭檔案
#import <libkern/OSAtomic.h>
OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
OSSpinLockUnlock(&lock);
OSSpinLockTry(&lock);
複製程式碼
自旋鎖存在優先順序反轉問題,OSSpinLock
是自旋鎖,也正是由於它是自旋鎖,所以容易發生優先順序反轉的問題。在ibireme的文章中已經寫到,當一個低優先順序執行緒獲得鎖的時候,如果此時一個高優先順序的系統到來,那麼會進入忙等狀態,不會進入睡眠,此時會一直佔用著系統CPU時間,導致低優先順序的無法拿到CPU時間片,從而無法完成任務也無法釋放鎖。除非能保證訪問鎖的執行緒全部處於同一優先順序,否則系統所有的自旋鎖都會出現優先順序反轉的問題。現在蘋果的OSSpinLock
已經被替換成
os_unfair_lock
typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);
複製程式碼
os_unfair_lock(OSSpinLock 替代品):
os_unfair_lock
是蘋果官方推薦的替換OSSpinLock
的方案,用於解決OSSpinLock
優先順序反轉問題,但是它在iOS10.0以上的系統才可以呼叫。
os_unfair_lock
非自旋鎖,是一個互斥鎖,引用SoC兄的文章os_unfair_lock的型別和自旋鎖與互斥鎖的比較,包括在在apple的官方文件裡面也是寫明的了,“ This function doesn't spin on contention, but instead waits in the kernel to be awoken by an unlock”,文中只是指出os_unfair_lock
是蘋果推出替代OSSpinLock
的一種相對高效的鎖,並不是說其是自旋鎖。
匯入標頭檔案#import< os/lock.h >
os_unfair_lock_t unfairLock;
unfairLock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(unfairLock);
os_unfair_lock_unlock(unfairLock);
os_unfair_lock_trylock(unfairLock);
複製程式碼
遞迴鎖
需要使用遞迴鎖的情況:
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveLock)(int);
RecursiveLock = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveLock(value - 1);
}
[lock unlock];
};
RecursiveLock(5);
});
複製程式碼
這段程式碼是一個典型的死鎖情況。在我們的執行緒中,RecursiveMethod是遞迴呼叫的。所有每次進入這個block時,都會去加一次鎖,而從第二次開始,由於鎖已經被使用了且沒有解鎖,所有它需要等待鎖被解除,這樣就導致了死鎖,執行緒被阻塞住了。導致crach
*** -[NSLock lock]: deadlock ( '(null)') *** Break on _NSLockError() to debug.
複製程式碼
NSRecursiveLock
遞迴鎖也是通過
pthread_mutex_lock
函式來實現,在函式內部會判斷鎖的型別,如果顯示是遞迴鎖,就允許遞迴呼叫,僅僅將一個計數器加一,鎖的釋放過程也是同理。
一個鎖可以被同一執行緒多次請求,而不會引起死鎖。這主要是用在迴圈或遞迴操作中。NSRecursiveLock
與NSLock
的區別在於內部封裝的pthread_mutex_t
物件的型別不同,前者的型別為PTHREAD_MUTEX_RECURSIVE
。
主要操作:
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
// 建立遞迴鎖[lock lockBeforeDate:date];
// 在給定的時間之前去嘗試請求一個鎖[lock tryLock];
// 嘗試去請求一個鎖,並會立即返回一個布林值,表示嘗試是否成功
另外,NSRecursiveLock還宣告瞭一個name屬性,如下:
@property(copy) NSString *name
我們可以使用這個字串來標識一個鎖。Cocoa也會使用這個name作為錯誤描述資訊的一部分。
條件鎖
NSCondition
封裝了一個互斥鎖和訊號量,它把前者的
lock
以及後者的wait
/signal
統一到NSCondition
物件中,是基於條件變數pthread_cond_t
來實現的,和訊號量相似,如果當前執行緒不滿足條件,那麼就會進入睡眠狀態,等待其他執行緒釋放鎖或者釋放訊號之後,就會喚醒執行緒。
NSCondition 的物件實際上作為一個鎖和一個執行緒檢查器:鎖主要為了當檢測條件時保護資料來源,執行條件引發的任務;執行緒檢查器主要是根據條件決定是否繼續執行執行緒,即執行緒是否被阻塞。
NSCondition
同樣實現了NSLocking
協議,所以它和NSLock
一樣,也有NSLocking協議的lock
和unlock
方法,可以當做NSLock
來使用解決執行緒同步問題,用法完全一樣。NSCondition
提供了wait
和signal
,和條件訊號量類似。比如我們要監聽array陣列的個數,當array的個數大於0的時候就執行清空操作。思路是這樣的,當array個數大於0時執行清空操作,否則,wait
等待執行清空操作。當array個數增加的時候發生signal
訊號,讓等待的執行緒喚醒繼續執行。NSCondition *lock = [[NSCondition alloc] init]; NSMutableArray *array = [[NSMutableArray alloc] init]; //消費者 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [lock lock]; while (!array.count) { [lock wait]; } [array removeAllObjects]; [lock unlock]; }); //生產者 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ sleep(1);//以保證讓執行緒2的程式碼後執行 [lock lock]; [array addObject:@1]; [lock signal]; [lock unlock]; }); 複製程式碼
NSCondition
可以給每個執行緒分別加鎖,加鎖後不影響其他執行緒進入臨界區。但是正是因為這種分別加鎖的方式,NSCondition使用wait並使用加鎖後並不能真正的解決資源的競爭。例如:不能讓m<0。假設當前m=0,執行緒A要判斷到m>0為假,執行等待;執行緒B執行了m=1操作,並喚醒執行緒A執行m-1操作的同時執行緒C判斷到m>0,因為他們在不同的執行緒鎖裡面,同樣判斷為真也執行了m-1,這個時候執行緒A和執行緒C都會執行m-1,但是m=1,結果就會造成m=-1.
NSCoditionLock
NSConditionLock也可以像NSCondition一樣做多執行緒之間的任務等待呼叫,而且是執行緒安全的。
NSConditionLock同樣實現了NSLocking協議,效能比較低。
NSConditonLock
內部持有了一個NSCondition
物件和_condition_value
屬性,當呼叫
- (instancetype)initWithCondition:(NSInteger)condition
初始化的時候會傳入一個condition
引數,該引數會賦值_condition_value
屬性
常用方法:
lock
不分條件,如果鎖沒被申請,直接執行程式碼lockBeforeDate:
在指定時間前嘗試加鎖,返回bool
lockWhenCondition:
滿足特定條件Condition
,加鎖執行相應程式碼lockWhenCondition: beforeDate:
和上條相同,增加時間戳tryLock
嘗試著加鎖,返回bool
tryLockWhenCondition:
,滿足特定條件Condition
,嘗試著加鎖,返回bool
unlock
不會清空條件,之後滿足條件的鎖還會執行unlockWithCondition:
設定解鎖條件(同一時刻只有一個條件,如果已經設定條件,相當於修改條件)
例項:
- (void)executeNSConditionLock {
NSConditionLock* lock = [[NSConditionLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (NSUInteger i=0; i<3; i++) {
sleep(2);
if (i == 2) {
[lock lock];
[lock unlockWithCondition:i];
}
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[self threadMethodOfNSCoditionLock:lock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[self threadMethodOfNSCoditionLock:lock];
});
}
-(void)threadMethodOfNSCoditionLock:(NSConditionLock*)lock{
[lock lockWhenCondition:2];
[lock unlock];
}
複製程式碼
讀寫鎖
讀寫鎖,在對檔案進行操作的時候,寫操作是排他的,一旦有多個執行緒對同一個檔案進行寫操作,後果不可估量,但讀是可以的,多個執行緒讀取時沒有問題的。
pthread_rwlock
讀寫鎖可以有三種狀態:
- 讀模式下加鎖狀態,
- 寫模式下加鎖狀態,
- 不加鎖狀態。
一次只有一個執行緒可以佔有寫模式的讀寫鎖,但是多個執行緒可用同時佔有讀模式的讀寫鎖。讀寫鎖也叫做共享-獨佔鎖,當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的,當它以寫模式鎖住時,它是以獨佔模式鎖住的。
因此:
- 當讀寫鎖被一個執行緒以讀模式佔用的時候,寫操作的其他執行緒會被阻塞,讀操作的其他執行緒還可以繼續進行
- 當讀寫鎖被一個執行緒以寫模式佔用的時候,寫操作的其他執行緒會被阻塞,讀操作的其他執行緒也被阻塞。
注意:
- 如果自己已經獲取了讀鎖,再去加寫鎖,會出現死鎖的
// 初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
//獲取一個讀出鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
//獲取一個寫入鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr); //釋放一個寫入鎖或者讀出鎖
//讀寫鎖屬性:
int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr)
int pthread_rwlock_destroy(pthread_rwlock_t *rwptr);
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *valptr);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int valptr);
複製程式碼
例項:
// 初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER
// 讀模式
pthread_rwlock_wrlock(&lock);
// 寫模式
pthread_rwlock_rdlock(&lock);
// 讀模式或者寫模式的解鎖
pthread_rwlock_unlock(&lock);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self readBookWithTag:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self readBookWithTag:2];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self writeBook:3];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self writeBook:4];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self readBookWithTag:5];
});
- (void)readBookWithTag:(NSInteger )tag {
pthread_rwlock_rdlock(&rwLock);
self.path = [[NSBundle mainBundle] pathForResource:@"1" ofType:@".doc"];
self.contentString = [NSString stringWithContentsOfFile:self.path encoding:NSUTF8StringEncoding error:nil];
pthread_rwlock_unlock(&rwLock);
}
- (void)writeBook:(NSInteger)tag {
pthread_rwlock_wrlock(&rwLock);
[self.contentString writeToFile:self.path atomically:YES encoding:NSUTF8StringEncoding error:nil];
pthread_rwlock_unlock(&rwLock);
}
複製程式碼
分散式鎖
NSDistributedLock
,分佈鎖,檔案方式實現,可以跨程式 用tryLock
方法獲取鎖。 用unlock
方法釋放鎖。如果一個獲取鎖的進行在釋放鎖之前掛了,那麼鎖就一直得不到釋放了,此時可以通過breakLock
強行獲取鎖。
注:這種鎖在ios上基本用不到,不過多探究。