在IOS上進行多線程開發,爲了保證線程安全,防止資源競爭,須要給進程進行加鎖,一般用到的進程鎖分爲7種。ios
- 信號量
- 互斥鎖
- 自旋鎖
- 遞歸鎖
- 條件鎖
- 讀寫鎖
- 分佈式鎖
鎖:是保證線程安全常見的同步工具,防止Data race(數據競爭)的發生。數組
Data race(數據競爭):安全
- 兩個或者更多線程在一個程序中,併發的訪問同一數據
- 至少一個訪問是寫入操做
- 些線程都不使用任何互斥鎖來控制這些訪問
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); // 釋放鎖
複製代碼
其中,鎖的屬性包含一下四種:bash
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切換也會消耗必定的時間。app
NSLock遵循NSLocking協議,同時也是互斥鎖,提供了lock
和unlock
方法來進行加鎖和解鎖。 NSLock內部是封裝了pthread_mutext
,類型是PTHREAD_MUTEXT_ERRORCHECK
,它會損失必定的性能換來錯誤提示。async
- (void)lock;
- (void)unlock;
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
複製代碼
tryLock
和lock
方法都會請求加鎖,惟一不一樣的是trylock
在沒有得到鎖的時候能夠繼續作一些任務和處理。lockBeforeDate:
方法也比較簡單,就是在limit時間點以前得到鎖,沒有拿到鎖就返回NO。分佈式
這實際上是一個 OC 層面的鎖,防止不一樣的線程同時執行同一段代碼,相比於使用 NSLock 建立鎖對象、加鎖和解鎖來講,
@synchronized
用着更方便,可讀性更高。
大致上,想要明白@synchronized
,須要知道在@synchronized
中objc_sync_enter
和objc_sync_exit
的成對調用,並且每一個傳入的對象,都會爲其分配一個遞歸鎖並存儲在哈希表中。在objc_sync_enter
中加鎖,在objc_sync_exit
中解鎖。
具體能夠參考這篇文章: 關於 @synchronized,這兒比你想知道的還要多函數
@synchronized(self) {
//數據操做
}
複製代碼
自旋鎖的目的是爲了確保臨界區只有一個線程能夠訪問。
當一個線程得到鎖以後,其餘線程將會一直循環在哪裏查看是否該鎖被釋放,是用於多線程同步的一種鎖,線程反覆檢查鎖變量是否可用。因爲線程在這一過程當中保持執行,所以是一種忙等待。一旦獲取了自旋鎖,線程會一直保持該鎖,直至顯式釋放自旋鎖。自旋鎖避免了進程上下文的調度開銷,所以對於線程只會阻塞很短期的場合是有效的。
因爲調用方會一直循環看該自旋鎖的的保持者是否已經釋放了資源,因此總的效率來講比互斥鎖高。可是自旋鎖只用於短期的資源訪問,若是不能短期內得到鎖,就會一直佔用着CPU,形成效率低下。
自旋鎖的一種,因爲在某些場景下不安全已被棄用。 需導入頭文件
#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
的方案,用於解決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.
複製代碼
遞歸鎖也是經過
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做爲錯誤描述信息的一部分。
封裝了一個互斥鎖和信號量,它把前者的
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.
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_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上基本用不到,不過多探究。