這兩天翻看 ibireme 大神 《不再安全的 OSSpinLock》 這篇文章,看到文中分析各種鎖之前的性能的圖表:
發(fā)現(xiàn)除了@synchronized
用過(guò),其他的都陌生的很,可以說(shuō)完全不知道....
于是懷著慚愧的心情趕緊把這些鎖學(xué)習(xí)了下,廢話不多說(shuō),我們開(kāi)始:
鎖 是什么意思?
我們?cè)谑褂枚嗑€程的時(shí)候多個(gè)線程可能會(huì)訪問(wèn)同一塊資源,這樣就很容易引發(fā)數(shù)據(jù)錯(cuò)亂和數(shù)據(jù)安全等問(wèn)題,這時(shí)候就需要我們保證每次只有一個(gè)線程訪問(wèn)這一塊資源,鎖 應(yīng)運(yùn)而生。
OSSpinLock
需導(dǎo)入頭文件:
#import <libkern/OSAtomic.h>
例子:
__block OSSpinLock oslock = OS_SPINLOCK_INIT;
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程1 準(zhǔn)備上鎖");
OSSpinLockLock(&oslock);
sleep(4);
NSLog(@"線程1");
OSSpinLockUnlock(&oslock);
NSLog(@"線程1 解鎖成功");
NSLog(@"--------------------------------------------------------");
});
//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程2 準(zhǔn)備上鎖");
OSSpinLockLock(&oslock);
NSLog(@"線程2");
OSSpinLockUnlock(&oslock);
NSLog(@"線程2 解鎖成功");
});
運(yùn)行結(jié)果:
我們來(lái)修改一下代碼:
__block OSSpinLock oslock = OS_SPINLOCK_INIT;
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
......
//OSSpinLockUnlock(&oslock);
......
運(yùn)行結(jié)果:
在 OSSpinLock1
圖中可以發(fā)現(xiàn):當(dāng)我們鎖住線程1時(shí),在同時(shí)鎖住線程2的情況下,線程2會(huì)一直等待(自旋鎖不會(huì)讓等待的進(jìn)入睡眠狀態(tài)),直到線程1的任務(wù)執(zhí)行完且解鎖完畢,線程2會(huì)立即執(zhí)行;而在 OSSpinLock2
圖中,因?yàn)槲覀冏⑨尩袅司€程1中的解鎖代碼,會(huì)繞過(guò)線程1,直到調(diào)用了線程2的解鎖方法才會(huì)繼續(xù)執(zhí)行線程1中的任務(wù),正常情況下,lock
和unlock
最好成對(duì)出現(xiàn)。
OS_SPINLOCK_INIT: 默認(rèn)值為
0
,在locked
狀態(tài)時(shí)就會(huì)大于0
,unlocked
狀態(tài)下為0
OSSpinLockLock(&oslock):上鎖,參數(shù)為OSSpinLock
地址
OSSpinLockUnlock(&oslock):解鎖,參數(shù)為OSSpinLock
地址
OSSpinLockTry(&oslock):嘗試加鎖,可以加鎖則立即加鎖并返回YES
,反之返回NO
這里順便提一下trylock
和lock
使用場(chǎng)景:
當(dāng)前線程鎖失敗,也可以繼續(xù)其它任務(wù),用 trylock 合適
當(dāng)前線程只有鎖成功后,才會(huì)做一些有意義的工作,那就 lock,沒(méi)必要輪詢 trylock
dispatch_semaphore 信號(hào)量
例子:
dispatch_semaphore_t signal = dispatch_semaphore_create(1); //傳入值必須 >=0, 若傳入為0則阻塞線程并等待timeout,時(shí)間到后會(huì)執(zhí)行其后的語(yǔ)句
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC);
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程1 等待ing");
dispatch_semaphore_wait(signal, overTime); //signal 值 -1
NSLog(@"線程1");
dispatch_semaphore_signal(signal); //signal 值 +1
NSLog(@"線程1 發(fā)送信號(hào)");
NSLog(@"--------------------------------------------------------");
});
//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程2 等待ing");
dispatch_semaphore_wait(signal, overTime);
NSLog(@"線程2");
dispatch_semaphore_signal(signal);
NSLog(@"線程2 發(fā)送信號(hào)");
});
dispatch_semaphore_create(1): 傳入值必須
>=0
, 若傳入為0
則阻塞線程并等待timeout,時(shí)間到后會(huì)執(zhí)行其后的語(yǔ)句
dispatch_semaphore_wait(signal, overTime):可以理解為lock
,會(huì)使得signal
值-1
dispatch_semaphore_signal(signal):可以理解為unlock
,會(huì)使得signal
值+1
關(guān)于信號(hào)量,我們可以用停車來(lái)比喻:
停車場(chǎng)剩余4個(gè)車位,那么即使同時(shí)來(lái)了四輛車也能停的下。如果此時(shí)來(lái)了五輛車,那么就有一輛需要等待。
信號(hào)量的值(signal)就相當(dāng)于剩余車位的數(shù)目,dispatch_semaphore_wait
函數(shù)就相當(dāng)于來(lái)了一輛車,dispatch_semaphore_signal
就相當(dāng)于走了一輛車。停車位的剩余數(shù)目在初始化的時(shí)候就已經(jīng)指明了(dispatch_semaphore_create(long value)),調(diào)用一次 dispatch_semaphore_signal,剩余的車位就增加一個(gè);調(diào)用一次dispatch_semaphore_wait 剩余車位就減少一個(gè);當(dāng)剩余車位為 0 時(shí),再來(lái)車(即調(diào)用 dispatch_semaphore_wait)就只能等待。有可能同時(shí)有幾輛車等待一個(gè)停車位。有些車主沒(méi)有耐心,給自己設(shè)定了一段等待時(shí)間,這段時(shí)間內(nèi)等不到停車位就走了,如果等到了就開(kāi)進(jìn)去停車。而有些車主就像把車停在這,所以就一直等下去。
運(yùn)行結(jié)果:
可以發(fā)現(xiàn),因?yàn)槲覀兂跏蓟盘?hào)量的時(shí)候是大于 0
的,所以并沒(méi)有阻塞線程,而是直接執(zhí)行了 線程1 線程2。
我們把 信號(hào)量初始值改為 0
:
dispatch_semaphore_t signal = dispatch_semaphore_create(0);
運(yùn)行結(jié)果:
可以看到這時(shí)候我們?cè)O(shè)置的 overTime
生效了。
pthread_mutex
ibireme 在《不再安全的 OSSpinLock》這篇文章中提到性能最好的 OSSpinLock
已經(jīng)不再是線程安全的并把自己開(kāi)源項(xiàng)目中的 OSSpinLock
都替換成了 pthread_mutex
。
特意去看了下源碼,總結(jié)了下常見(jiàn)用法:
使用需導(dǎo)入頭文件:
#import <pthread.h>
例子:
static pthread_mutex_t pLock;
pthread_mutex_init(&pLock, NULL);
//1.線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程1 準(zhǔn)備上鎖");
pthread_mutex_lock(&pLock);
sleep(3);
NSLog(@"線程1");
pthread_mutex_unlock(&pLock);
});
//1.線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程2 準(zhǔn)備上鎖");
pthread_mutex_lock(&pLock);
NSLog(@"線程2");
pthread_mutex_unlock(&pLock);
});
運(yùn)行結(jié)果:
pthread_mutex 中也有個(gè)
pthread_mutex_trylock(&pLock)
,和上面提到的OSSpinLockTry(&oslock)
區(qū)別在于,前者可以加鎖時(shí)返回的是0
,否則返回一個(gè)錯(cuò)誤提示碼;后者返回的YES
和NO
這里貼個(gè) YYKit 中的源碼:
pthread_mutex(recursive)
經(jīng)過(guò)上面幾種例子,我們可以發(fā)現(xiàn):加鎖后只能有一個(gè)線程訪問(wèn)該對(duì)象,后面的線程需要排隊(duì),并且 lock 和 unlock 是對(duì)應(yīng)出現(xiàn)的,同一線程多次 lock 是不允許的,而遞歸鎖允許同一個(gè)線程在未釋放其擁有的鎖時(shí)反復(fù)對(duì)該鎖進(jìn)行加鎖操作。
例子:
static pthread_mutex_t pLock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //初始化attr并且給它賦予默認(rèn)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //設(shè)置鎖類型,這邊是設(shè)置為遞歸鎖
pthread_mutex_init(&pLock, &attr);
pthread_mutexattr_destroy(&attr); //銷毀一個(gè)屬性對(duì)象,在重新進(jìn)行初始化之前該結(jié)構(gòu)不能重新使用
//1.線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
pthread_mutex_lock(&pLock);
if (value > 0) {
NSLog(@"value: %d", value);
RecursiveBlock(value - 1);
}
pthread_mutex_unlock(&pLock);
};
RecursiveBlock(5);
});
運(yùn)行結(jié)果:
上面的代碼如果我們用
pthread_mutex_init(&pLock, NULL)
初始化會(huì)出現(xiàn)死鎖的情況,遞歸鎖能很好的避免這種情況的死鎖;
NSLock
NSLock API 很少也很簡(jiǎn)單:
lock、unlock:不多做解釋,和上面一樣
trylock:能加鎖返回 YES 并執(zhí)行加鎖操作,相當(dāng)于 lock,反之返回 NO
** lockBeforeDate:這個(gè)方法表示會(huì)在傳入的時(shí)間內(nèi)嘗試加鎖,若能加鎖則執(zhí)行加鎖**操作并返回 YES,反之返回 NO
例子:
NSLock *lock = [NSLock new];
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程1 嘗試加速ing...");
[lock lock];
sleep(3);//睡眠5秒
NSLog(@"線程1");
[lock unlock];
NSLog(@"線程1解鎖成功");
});
//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程2 嘗試加速ing...");
BOOL x = [lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:4]];
if (x) {
NSLog(@"線程2");
[lock unlock];
}else{
NSLog(@"失敗");
}
});
運(yùn)行結(jié)果:
NSCondition
我們先來(lái)看看 API:
看字面意思很好理解:
wait:進(jìn)入等待狀態(tài)
waitUntilDate::讓一個(gè)線程等待一定的時(shí)間
signal:?jiǎn)拘岩粋€(gè)等待的線程
broadcast:?jiǎn)拘阉械却木€程
例子:
- 等待2秒
NSCondition *cLock = [NSCondition new];
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"start");
[cLock lock];
[cLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];
NSLog(@"線程1");
[cLock unlock];
});
結(jié)果:
- 喚醒一個(gè)等待線程
NSCondition *cLock = [NSCondition new];
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[cLock lock];
NSLog(@"線程1加鎖成功");
[cLock wait];
NSLog(@"線程1");
[cLock unlock];
});
//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[cLock lock];
NSLog(@"線程2加鎖成功");
[cLock wait];
NSLog(@"線程2");
[cLock unlock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(2);
NSLog(@"喚醒一個(gè)等待的線程");
[cLock signal];
});
結(jié)果:
- 喚醒所有等待的線程
.........
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(2);
NSLog(@"喚醒所有等待的線程");
[cLock broadcast];
});
運(yùn)行結(jié)果:
NSRecursiveLock
上面已經(jīng)大概介紹過(guò)了:
遞歸鎖可以被同一線程多次請(qǐng)求,而不會(huì)引起死鎖。這主要是用在循環(huán)或遞歸操作中。
例子:
NSLock *rLock = [NSLock new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
[rLock lock];
if (value > 0) {
NSLog(@"線程%d", value);
RecursiveBlock(value - 1);
}
[rLock unlock];
};
RecursiveBlock(4);
});
運(yùn)行結(jié)果:
這段代碼是一個(gè)典型的死鎖情況。在我們的線程中,RecursiveMethod
是遞歸調(diào)用的。所以每次進(jìn)入這個(gè) block 時(shí),都會(huì)去加一次鎖,而從第二次開(kāi)始,由于鎖已經(jīng)被使用了且沒(méi)有解鎖,所以它需要等待鎖被解除,這樣就導(dǎo)致了死鎖,線程被阻塞住了。
將 NSLock 替換為 NSRecursiveLock:
NSRecursiveLock *rLock = [NSRecursiveLock new];
..........
運(yùn)行結(jié)果:
NSRecursiveLock 方法里還提供了兩個(gè)方法,用法和上面介紹的基本沒(méi)什么差別,這里不過(guò)多介紹了:
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@synchronized
@synchronized 相信大家應(yīng)該都熟悉,它的用法應(yīng)該算這些鎖中最簡(jiǎn)單的:
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized (self) {
sleep(2);
NSLog(@"線程1");
}
});
//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized (self) {
NSLog(@"線程2");
}
});
有興趣可以看一下這篇文章 《 關(guān)于 @synchronized,這兒比你想知道的還要多》
NSConditionLock 條件鎖
我們先來(lái)看看 API :
相比于 NSLock 多了個(gè) condition
參數(shù),我們可以理解為一個(gè)條件標(biāo)示。
例子:
NSConditionLock *cLock = [[NSConditionLock alloc] initWithCondition:0];
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if([cLock tryLockWhenCondition:0]){
NSLog(@"線程1");
[cLock unlockWithCondition:1];
}else{
NSLog(@"失敗");
}
});
//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[cLock lockWhenCondition:3];
NSLog(@"線程2");
[cLock unlockWithCondition:2];
});
//線程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[cLock lockWhenCondition:1];
NSLog(@"線程3");
[cLock unlockWithCondition:3];
});
運(yùn)行結(jié)果:
- 我們?cè)诔跏蓟?NSConditionLock 對(duì)象時(shí),給了他的標(biāo)示為
0
- 執(zhí)行
tryLockWhenCondition:
時(shí),我們傳入的條件標(biāo)示也是0
,所 以線程1 加鎖成功 - 執(zhí)行
unlockWithCondition:
時(shí),這時(shí)候會(huì)把condition
由0
修改為1
- 因?yàn)?code>condition 修改為了
1
, 會(huì)先走到 線程3,然后 線程3 又將condition
修改為3
- 最后 走了 線程2 的流程
從上面的結(jié)果我們可以發(fā)現(xiàn),NSConditionLock 還可以實(shí)現(xiàn)任務(wù)之間的依賴。
參考文獻(xiàn):
NSRecursiveLock遞歸鎖的使用
關(guān)于dispatch_semaphore的使用
實(shí)現(xiàn)鎖的多種方式和鎖的高級(jí)用法