背景:最近在項(xiàng)目中涉及到很多鎖方面的知識(shí)号阿,于是在項(xiàng)目完成后圣拄,決定把最近關(guān)于鎖的知識(shí)記錄下來(lái)纽什,方便自己以后查詢
- 鎖的相關(guān)概念
- 鎖的性能比較
- 鎖的用法
- 各種鎖的優(yōu)缺點(diǎn)
概念
0.鎖: 語(yǔ)言的并發(fā)程序設(shè)計(jì)馅笙,一直是一個(gè)比較困難且不可避免的話題。多數(shù)程序員都會(huì)嘗試使用多線程編程蔬捷,但是卻很難保證自己所寫的多線程程序的正確性垄提。多線程程序,如果涉及到對(duì)共享資源的并發(fā)讀寫周拐,就會(huì)產(chǎn)生資源爭(zhēng)用铡俐。解決資源爭(zhēng)用,最直接的想法是引入鎖妥粟,對(duì)并發(fā)讀寫的數(shù)據(jù)進(jìn)行保護(hù)审丘。
1.死鎖:死鎖是指兩個(gè)或兩個(gè)以上的進(jìn)程在執(zhí)行過(guò)程中,由于競(jìng)爭(zhēng)資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象勾给,若無(wú)外力作用滩报,它們都將無(wú)法推進(jìn)下去。此時(shí)稱系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖播急,這些永遠(yuǎn)在互相等待的進(jìn)程稱為死鎖進(jìn)程脓钾。當(dāng)然,產(chǎn)生死鎖有四個(gè)條件桩警,有需要深入了解的我在后文給鏈接
2.互斥鎖:最常使用于線程同步的鎖可训;標(biāo)記用來(lái)保證在任一時(shí)刻,只能有一個(gè)線程訪問(wèn)該對(duì)象,同一線程多次加鎖操作會(huì)造成死鎖沉噩;臨界區(qū)和互斥量都可用來(lái)實(shí)現(xiàn)此鎖捺宗,通常情況下鎖操作失敗會(huì)將該線程睡眠等待鎖釋放時(shí)被喚醒
3.自旋鎖:同樣用來(lái)標(biāo)記只能有一個(gè)線程訪問(wèn)該對(duì)象,在同一線程多次加鎖操作會(huì)造成死鎖川蒙;同互斥鎖不同的是在鎖操作需要等待的時(shí)候并不是睡眠等待喚醒蚜厉,而是循環(huán)檢測(cè)保持者已經(jīng)釋放了鎖,這樣做的好處是節(jié)省了線程從睡眠狀態(tài)到喚醒之間內(nèi)核會(huì)產(chǎn)生的消耗畜眨,在加鎖時(shí)間短暫的環(huán)境下這點(diǎn)會(huì)提高很大效率
4.遞歸鎖:嚴(yán)格上講遞歸鎖只是互斥鎖的一個(gè)特例昼牛,同樣只能有一個(gè)線程訪問(wèn)該對(duì)象,但允許同一個(gè)線程在未釋放其擁有的鎖時(shí)反復(fù)對(duì)該鎖進(jìn)行加鎖操作康聂;
iOS常用鎖的性能比較
這個(gè)問(wèn)題我沒有去測(cè)試贰健,引用一張大神測(cè)試后的照片截圖這是參考式鏈接:
[libireme 不再安全的 OSSpinLock]:
https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/
iOS 鎖的用法
OSSpinLock
OSSpinLock 使用方法
- (void)testOSSpinLock{
__block OSSpinLock oslock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程1 準(zhǔn)備上鎖");
OSSpinLockLock(&oslock);
NSLog(@"線程1 執(zhí)行任務(wù)");
OSSpinLockUnlock(&oslock);
NSLog(@"線程1 解鎖成功");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程2 準(zhǔn)備上鎖");
OSSpinLockLock(&oslock);
NSLog(@"線程2 執(zhí)行任務(wù)");
OSSpinLockUnlock(&oslock);
NSLog(@"線程2 解鎖成功");
});
}
OSSpinLock 存在問(wèn)題
新版 iOS 中,系統(tǒng)維護(hù)了 5 個(gè)不同的線程優(yōu)先級(jí)/QoS: background恬汁,utility伶椿,default,user-initiated氓侧,user-interactive脊另。高優(yōu)先級(jí)線程始終會(huì)在低優(yōu)先級(jí)線程前執(zhí)行,一個(gè)線程不會(huì)受到比它更低優(yōu)先級(jí)線程的干擾约巷。這種線程調(diào)度算法會(huì)產(chǎn)生潛在的優(yōu)先級(jí)反轉(zhuǎn)問(wèn)題偎痛,從而破壞了 spin lock。
具體來(lái)說(shuō)独郎,如果一個(gè)低優(yōu)先級(jí)的線程獲得鎖并訪問(wèn)共享資源踩麦,這時(shí)一個(gè)高優(yōu)先級(jí)的線程也嘗試獲得這個(gè)鎖,它會(huì)處于 spin lock 的忙等狀態(tài)從而占用大量 CPU氓癌。此時(shí)低優(yōu)先級(jí)線程無(wú)法與高優(yōu)先級(jí)線程爭(zhēng)奪 CPU 時(shí)間谓谦,從而導(dǎo)致任務(wù)遲遲完不成、無(wú)法釋放 lock顽铸。這并不只是理論上的問(wèn)題茁计,libobjc 已經(jīng)遇到了很多次這個(gè)問(wèn)題了,于是蘋果的工程師停用了 OSSpinLock谓松。
蘋果工程師 Greg Parker 提到星压,對(duì)于這個(gè)問(wèn)題,一種解決方案是用 truly unbounded backoff 算法鬼譬,這能避免 livelock 問(wèn)題娜膘,但如果系統(tǒng)負(fù)載高時(shí),它仍有可能將高優(yōu)先級(jí)的線程阻塞數(shù)十秒之久优质;另一種方案是使用 handoff lock 算法竣贪,這也是 libobjc 目前正在使用的军洼。鎖的持有者會(huì)把線程 ID 保存到鎖內(nèi)部,鎖的等待者會(huì)臨時(shí)貢獻(xiàn)出它的優(yōu)先級(jí)來(lái)避免優(yōu)先級(jí)反轉(zhuǎn)的問(wèn)題演怎。理論上這種模式會(huì)在比較復(fù)雜的多鎖條件下產(chǎn)生問(wèn)題匕争,但實(shí)踐上目前還一切都好。
libobjc 里用的是 Mach 內(nèi)核的 thread_switch() 然后傳遞了一個(gè) mach thread port 來(lái)避免優(yōu)先級(jí)反轉(zhuǎn)爷耀,另外它還用了一個(gè)私有的參數(shù)選項(xiàng)甘桑,所以開發(fā)者無(wú)法自己實(shí)現(xiàn)這個(gè)鎖。另一方面歹叮,由于二進(jìn)制兼容問(wèn)題跑杭,OSSpinLock 也不能有改動(dòng)。
最終的結(jié)論就是咆耿,除非開發(fā)者能保證訪問(wèn)鎖的線程全部都處于同一優(yōu)先級(jí)德谅,否則 iOS 系統(tǒng)中所有類型的自旋鎖都不能再使用了。
這是參考式鏈接:
[libireme 不再安全的 OSSpinLock]:
https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/
dispatch_semaphore
dispatch_semaphore
信號(hào)量是一個(gè)整形值并且具有一個(gè)初始計(jì)數(shù)值萨螺,并且支持兩個(gè)操作:信號(hào)通知和等待窄做。當(dāng)一個(gè)信號(hào)量被信號(hào)通知,其計(jì)數(shù)會(huì)被增加屑迂。當(dāng)一個(gè)線程在一個(gè)信號(hào)量上等待時(shí)浸策,線程會(huì)被阻塞(如果有必要的話),直至計(jì)數(shù)器大于零惹盼,然后線程會(huì)減少這個(gè)計(jì)數(shù),在GCD中有三個(gè)函數(shù)是semaphore的操作
dispatch_semaphore_create(傳入值): 傳入值必須 >=0, 若傳入為 0 則阻塞線程并等待timeout,時(shí)間到后會(huì)執(zhí)行其后的語(yǔ)句
dispatch_semaphore_wait(signal, overTime):可以理解為 lock,會(huì)使得 signal 值 -1
dispatch_semaphore_signal(signal):可以理解為 unlock,會(huì)使得 signal 值 +1
- (void)dispatchSemaphore{
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
//等待時(shí)間
dispatch_time_t overTime = DISPATCH_TIME_FOREVER;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//等同加鎖 -1
dispatch_semaphore_wait(signal, overTime);
NSLog(@"線程1 執(zhí)行任務(wù)");
sleep(10);
//發(fā)通知 等同解鎖 +1
dispatch_semaphore_signal(signal);
NSLog(@"線程1 解鎖");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//等同加鎖 -1
dispatch_semaphore_wait(signal, overTime);
NSLog(@"線程2 執(zhí)行任務(wù)");
//發(fā)通知 等同解鎖 +1
dispatch_semaphore_signal(signal);
NSLog(@"線程2 解鎖");
});
}
NSCondition
NSCondition 的對(duì)象實(shí)際上作為一個(gè)鎖和一個(gè)線程檢查器:鎖主要為了當(dāng)檢測(cè)條件時(shí)保護(hù)數(shù)據(jù)源,執(zhí)行條件引發(fā)的任務(wù)惫确;線程檢查器主要是根據(jù)條件決定是否繼續(xù)運(yùn)行線程手报,即線程是否被阻塞。
- wait:進(jìn)入等待狀態(tài)
- waitUntilDate::讓一個(gè)線程等待一定的時(shí)間
- signal:?jiǎn)拘岩粋€(gè)等待的線程
- broadcast:?jiǎn)拘阉械却木€程
NSCondition 使用方法
假定:有A改化、B......兩個(gè)個(gè)任務(wù)掩蛤,沒有執(zhí)行順序,或者只能確定確定一個(gè)執(zhí)行任務(wù),看下面代碼陈肛,以及執(zhí)行結(jié)果
- (void)testConditionLock{
NSCondition *condition = [[NSCondition alloc] init];
//線程一
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condition lock];
NSLog(@"線程一 加鎖成功");
[condition wait];
NSLog(@"線程一 執(zhí)行任務(wù)");
[condition unlock];
NSLog(@"線程一 解鎖");
[condition signal];
});
//線程二
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condition lock];
NSLog(@"線程二 加鎖成功");
[condition wait];
NSLog(@"線程二 執(zhí)行任務(wù)");
[condition unlock];
NSLog(@"線程二 解鎖");
[condition signal];
});
//線程三
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condition lock];
NSLog(@"線程三 加鎖成功");
//[condition wait];
NSLog(@"線程三 執(zhí)行任務(wù)");
[condition unlock];
NSLog(@"線程三 解鎖");
[condition signal];
});
}
結(jié)果
2018-03-21 17:06:23.156804+0800 Lock-test[74191:6464691] 線程一 加鎖成功
2018-03-21 17:06:23.157058+0800 Lock-test[74191:6464694] 線程二 加鎖成功
2018-03-21 17:06:23.157413+0800 Lock-test[74191:6464693] 線程三 加鎖成功
2018-03-21 17:06:23.158964+0800 Lock-test[74191:6464693] 線程三 執(zhí)行任務(wù)
2018-03-21 17:06:23.159106+0800 Lock-test[74191:6464693] 線程三 解鎖
2018-03-21 17:06:23.159695+0800 Lock-test[74191:6464691] 線程一 執(zhí)行任務(wù)
2018-03-21 17:06:23.159942+0800 Lock-test[74191:6464691] 線程一 解鎖
2018-03-21 17:06:23.160125+0800 Lock-test[74191:6464694] 線程二 執(zhí)行任務(wù)
2018-03-21 17:06:23.160249+0800 Lock-test[74191:6464694] 線程二 解鎖
NSCondition 缺點(diǎn):
不能保證任務(wù)有序執(zhí)行揍鸟,只能確保第一執(zhí)行的任務(wù)
NSConditionLock
條件鎖,一個(gè)線程獲得了鎖句旱,其它線程等待
tryLockWhenCondition:表示如果沒有其他線程獲得該鎖阳藻,但是該鎖內(nèi)部的condition不等于條件,它依然不能獲得鎖谈撒,仍然等待
unlockWithCondition:: 解鎖成功腥泥,并指向下個(gè)一條件
NSConditionLock 使用方法
假定:有A、B啃匿、C三個(gè)任務(wù)蛔外,需要執(zhí)行的順序是B->A->C
- (void)testConditonLock{
NSConditionLock *condition = [[NSConditionLock alloc] initWithCondition:2];
//線程一
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程一 加鎖");
[condition lockWhenCondition:1];
NSLog(@"線程一 執(zhí)行任務(wù)");
[condition unlockWithCondition:3];
});
//線程二
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程二 加鎖");
if([condition tryLockWhenCondition:2]){
NSLog(@"線程二 執(zhí)行任務(wù)");
}else{
NSLog(@"執(zhí)行任務(wù)失敗");
}
[condition unlockWithCondition:1];
});
//線程三
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"線程三 加鎖");
[condition lockWhenCondition:3];
NSLog(@"線程三 執(zhí)行任務(wù)");
// [condition unlockWithCondition:3];
});
}
2018-03-21 17:23:45.333313+0800 Lock-test[74523:6479604] 線程二 加鎖
2018-03-21 17:23:45.333313+0800 Lock-test[74523:6479607] 線程一 加鎖
2018-03-21 17:23:45.333313+0800 Lock-test[74523:6479605] 線程三 加鎖
2018-03-21 17:23:45.333687+0800 Lock-test[74523:6479604] 線程二 執(zhí)行任務(wù)
2018-03-21 17:23:45.335576+0800 Lock-test[74523:6479607] 線程一 執(zhí)行任務(wù)
2018-03-21 17:23:45.335783+0800 Lock-test[74523:6479605] 線程三 執(zhí)行任務(wù)
NSConditionLock 優(yōu)點(diǎn):
可以在加鎖的同時(shí)蛆楞,保證任務(wù)有序進(jìn)行。缺點(diǎn)可能就是所謂性能低了
synchronized
synchronized 遞歸鎖夹厌,最常見豹爹,也是最常用的鎖。
- (void)testSYnchronized{
//線程一
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(self){
NSLog(@"線程一");
[self test:@"=="];
}
});
//線程二
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(self){
NSLog(@"線程二");
[self test:@"================================="];
}
});
}
- (void)test:(NSString *)test{
while (1) {
NSLog(@"%@", test);
}
}
參考資料
https://juejin.im/entry/5a00f59ff265da4314401967
http://blog.csdn.net/qq_30513483/article/details/52349968
https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/