鎖在我們開發(fā)中用的相對(duì)比較少漏麦,但是作為一個(gè)開發(fā)者,還是需要了解鎖的原理;
下圖是鎖的性能數(shù)據(jù)圖:
鎖的歸類
- 自旋鎖:線程反復(fù)檢查鎖變量是否可用昧捷。由于線程在這一過程中保持執(zhí)行, 因此是一種忙等待罐寨。一旦獲取了自旋鎖靡挥,線程會(huì)一直保持該鎖,直至顯式釋 放自旋鎖鸯绿。 自旋鎖避免了進(jìn)程上下文的調(diào)度開銷跋破,因此對(duì)于線程只會(huì)阻塞很 短時(shí)間的場(chǎng)合是有效的。
- 互斥鎖:是一種用于多線程編程中瓶蝴,防止兩條線程同時(shí)對(duì)同一公共資源(比 如全局變量)進(jìn)行讀寫的機(jī)制毒返。該目的通過將代碼切片成一個(gè)一個(gè)的臨界區(qū) 而達(dá)成
- 條件鎖:就是條件變量,當(dāng)進(jìn)程的某些資源要求不滿足時(shí)就進(jìn)入休眠舷手,也就是鎖住了饿悬。當(dāng)資源被分配到了,條件鎖打開聚霜,進(jìn)程繼續(xù)運(yùn)行
- 遞歸鎖:就是同一個(gè)線程可以加鎖N次而不會(huì)引發(fā)死鎖
- 信號(hào)量:是一種更高級(jí)的同步機(jī)制狡恬,互斥鎖可以說是semaphore在僅取值0/1時(shí)的特例。信號(hào)量可以有更多的取值空間蝎宇,用來實(shí)現(xiàn)更加復(fù)雜的同步弟劲,而不單單是線程間互斥
- 讀寫鎖:讀寫鎖實(shí)際是一種特殊的自旋鎖,它把對(duì)共享資源的訪問者劃分成讀者和寫者姥芥,讀者只對(duì)共享資源 進(jìn)行讀訪問兔乞,寫者則需要對(duì)共享資源進(jìn)行寫操作,這種鎖相對(duì)于自旋鎖而言凉唐,能提高并發(fā)性庸追,它允許同時(shí)有多個(gè)讀者來訪問共享資源。
其實(shí)基本的鎖就包括了三類 自旋鎖 互斥鎖 讀寫鎖台囱,
其他的比如條件鎖淡溯,遞歸鎖,信號(hào)量都是上層的封裝和實(shí)現(xiàn)!
- 互斥鎖在上圖包括:NSLock簿训,pthread_mutex咱娶,@synchronized
- 條件鎖有:NSCondition米间,NSConditionLock
- 遞歸鎖:NSRecursiveLock,pthread_mutex(recursive)
- 信號(hào)量:dispatch_semaphore
互斥鎖與遞歸鎖
在開發(fā)中膘侮,我們常用的大概是@synchronized
屈糊,就從這個(gè)開始講解;
下面使用@synchronized
來舉一個(gè)例子:
鎖的應(yīng)用是為了線程的安全執(zhí)行琼了,例如購(gòu)票逻锐,不同線程購(gòu)票不加鎖的話,會(huì)出現(xiàn)同一張票被賣多次雕薪。
下面看一段代碼:
- (void)saleTicket{
@synchronized (self) {
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"當(dāng)前余票還剩:%ld張",self.ticketCount)
}else{
NSLog(@"當(dāng)前車票已售罄");
}
}
}
通過@synchronized
對(duì)票數(shù)的減少進(jìn)行加鎖之后谦去,我們執(zhí)行程序后,就不會(huì)出現(xiàn)問題蹦哼。
下圖是執(zhí)行后的打印結(jié)果:
那既然@synchronized
能夠保證線程的安全鳄哭,那就先去看一下它的底層原理;
首先創(chuàng)建一個(gè)iOS工程纲熏,在main.m
函數(shù)中加上@synchronized (appDelegateClassName) { }
這句代碼妆丘,對(duì)main函數(shù)進(jìn)行xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m
轉(zhuǎn)換成cpp
文件。
接下來就看一下main函數(shù)中的@synchronized
轉(zhuǎn)化之后變成的代碼塊:
{
id _rethrow = 0;
id _sync_obj = (id)appDelegateClassName;
objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {} ~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
id sync_exit;
}
_sync_exit(_sync_obj);
} catch (id e) {_
rethrow = e;
}
{
struct _FIN {
_FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
}
_fin_force_rethow(_rethrow);}
}
可以看到它有一個(gè)objc_sync_enter
和_sync_exit
局劲;
通過對(duì)objc_sync_enter
的斷點(diǎn)查看勺拣,得知@synchronized
的底層在libobjc.A.dylib
中。
下圖是objc_sync_enter
的源碼實(shí)現(xiàn)鱼填,可以看到它傳入了obj
之后药有,就會(huì)進(jìn)行處理,而沒有傳入時(shí)苹丸,就會(huì)調(diào)用objc_sync_nil()
函數(shù)愤惰,這個(gè)函數(shù)沒有任何實(shí)現(xiàn)。
也就是說赘理,當(dāng)@synchronized(nil)
括號(hào)中傳入的是nil宦言,它什么都不做,無法起作用商模。
在有值的情況下奠旺,會(huì)對(duì)mutex
進(jìn)行lock()
加鎖函數(shù)的調(diào)用,那么看一下objc_sync_exit
的函數(shù)實(shí)現(xiàn):
可以看到在objc_sync_exit
中會(huì)對(duì)mutex
調(diào)用tryUnlock()
解鎖函數(shù)。
那么重點(diǎn)就是SyncData
了施流,它是一個(gè)結(jié)構(gòu)體:
而結(jié)構(gòu)體中最重要的就是recursive_mutex_t
:
它是一個(gè)recursive_mutex_tt
類响疚,在這個(gè)類中有lock
和unlock
兩個(gè)方法,是一個(gè)遞歸鎖瞪醋。
那么回到objc_sync_enter
忿晕,鎖的創(chuàng)建是由id2data
創(chuàng)建的,這個(gè)函數(shù)代碼很多趟章,下圖是一部分代碼杏糙,主要功能是通過kvc的方式拿到data值;
在if(data)
判斷中蚓土,有對(duì)ACQUIRE
和RELEASE
的方法實(shí)現(xiàn):
其中lockCount
表示被鎖了多少次宏侍,可重入,比遞歸鎖功能更強(qiáng)大蜀漆,因?yàn)樵诓榭?code>SyncData時(shí)谅河,它是一個(gè)鏈表結(jié)構(gòu)。
上面的代碼是在SUPPORT_DIRECT_THREAD_KEYS
的情況下的确丢,而不在這個(gè)情況下绷耍,也跟上面的代碼差不多;
SUPPORT_DIRECT_THREAD_KEYS
是從線程棧緩存的形式鲜侥,而endif
是從cache
的形式獲取去緩存褂始。
而如果第一次加載時(shí),就會(huì)從下面的代碼執(zhí)行:
也就是說描函,第一次進(jìn)來崎苗,會(huì)通過kvc對(duì)tls進(jìn)行設(shè)值和標(biāo)記,設(shè)置threadCount = 1
舀寓,lockCount = 1
胆数,在線程棧存空間和緩存空間中都會(huì)進(jìn)行處理。
那么一個(gè)完整的流程也就很清晰了互墓。
總結(jié):@synchronized整個(gè)流程其實(shí)就是一張哈希表必尼,因?yàn)榈讓臃庋b的是recursive_mutex_t
,所以是一把遞歸鎖,擴(kuò)展了遞歸鎖里面增加了lockCount
篡撵,防止多線程的重入判莉,增加了threadCount
進(jìn)行處理。
@synchronized
這把鎖的性能是比較低的育谬,因?yàn)槔锩嬗泻芏噫湵淼牟樵兟钭猓彺妫聦哟a的查找斑司,導(dǎo)致了性能是比較差渗饮;但是為什么用的多呢,因?yàn)榉奖愫?jiǎn)單宿刮,好用互站。
下面來看一段代碼:
for (int i = 0; i < 200; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
_testArray = [NSMutableArray array];
});
}
這一段代碼是使用異步函數(shù)創(chuàng)建數(shù)組對(duì)象,這一段代碼是有問題的僵缺,因?yàn)椴粩嗟某跏蓟瘜?dǎo)致了問題胡桃,沒銷毀就創(chuàng)建,多個(gè)線程創(chuàng)建同一個(gè)對(duì)象磕潮,釋放一次還好翠胰,釋放多次容贝,僵尸對(duì)象,就造成野指針之景。
那么防止問題的產(chǎn)生可以進(jìn)行加鎖斤富;例如使用@synchronized()
,那么括號(hào)內(nèi)的參數(shù)可以填什么呢?
如果是填_testArray
锻狗,那么還是會(huì)存在問題满力,因?yàn)榇嬖谀骋粋€(gè)臨界點(diǎn),_testArray
會(huì)變成nil轻纪,那么鎖nil就會(huì)出問題油额;可以進(jìn)行鎖self,因?yàn)?code>self是持有者刻帚,它是有一個(gè)生命周期的潦嘶。
那么除了用@synchronized
,在性能和對(duì)objc的生命周期不明確時(shí)崇众,還可以使用NSLock
衬以;
在創(chuàng)建數(shù)組前執(zhí)行[lock lock]
,在創(chuàng)建之后執(zhí)行[lock unlock]
;
下面來研究一下NSRecursiveLock
和NSLock
這兩把鎖的使用,看下面一段代碼:
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
NSLock *lock = [[NSLock alloc] init];
for (int i= 0; i<100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
});
}
這段代碼進(jìn)行了函數(shù)的嵌套調(diào)用校摩,會(huì)產(chǎn)生遞歸看峻,那么如何加鎖使其不產(chǎn)生遞歸呢?
通常一般情況下衙吩,我們會(huì)在方法執(zhí)行前加鎖和方法執(zhí)行結(jié)束進(jìn)行解鎖互妓,也就是說在testMethod
函數(shù)前調(diào)用[lock lock]
在testMethod(10)
之后執(zhí)行[lock unlock]
;那這樣做產(chǎn)生的后果就是它會(huì)循環(huán)10次從10到1的結(jié)果坤塞,這是一種解決方法冯勉;
那如果在testMethod
方法里的if
判斷外加鎖在if
判斷外解鎖,這樣會(huì)產(chǎn)生循環(huán)遞歸問題摹芙,因?yàn)?code>Lock是一把簡(jiǎn)單的互斥鎖灼狰,當(dāng)執(zhí)行testMethod
進(jìn)行加鎖,之后又調(diào)用testMethod
浮禾,又一次加鎖交胚,也就是不同的線程進(jìn)行加鎖,當(dāng)想要解鎖時(shí)盈电,因?yàn)槠渌€程已經(jīng)加鎖了蝴簇,需要等待其他線程進(jìn)行解鎖。
那么NSLock
是無法解決這種遞歸的特性匆帚,使用@synchronized
也是可以的熬词,但是效果也是執(zhí)行10次從10到1。
那么使用NSRecursiveLock
這把遞歸鎖,是可以很好的解決這個(gè)問題的互拾。
在testMethod
方法前調(diào)用[recursiveLock lock];
在if
判斷結(jié)束后調(diào)用[recursiveLock unlock];
是可以解決問題的歪今。
NSRecursiveLock
和Lock
的底層都是在pthread
的基礎(chǔ)上進(jìn)行封裝的;而他們的代碼實(shí)現(xiàn)大部分是一樣的颜矿,但是NSRecursiveLock
的初始化的地方與NSLock
是有區(qū)別的寄猩,看下面兩張圖片:
底層源碼是來自swift的Foundation框架。
那么關(guān)于NSLock
和NSRecursiveLock
的使用就介紹完了或衡,總的來說焦影,使用是比較麻煩的车遂,在使用便捷方面封断,@synchronized
更簡(jiǎn)單、實(shí)用舶担,而這兩把鎖就比較復(fù)雜坡疼,但是性能會(huì)比較高一些。
條件鎖
下面來研究一下NSCondition
這把鎖衣陶;
1:[condition lock];//一般用于多線程同時(shí)訪問柄瑰、修改同一個(gè)數(shù)據(jù)源,保證在同一 時(shí)間內(nèi)數(shù)據(jù)源只被訪問剪况、修改一次教沾,其他線程的命令需要在lock 外等待,只到 unlock 译断,才可訪問
2:[condition unlock];//與lock 同時(shí)使用
3:[condition wait];//讓當(dāng)前線程處于等待狀態(tài)
4:[condition signal];//CPU發(fā)信號(hào)告訴線程不用在等待授翻,可以繼續(xù)執(zhí)行
NSCondition的對(duì)象實(shí)際上作為一個(gè)鎖和一個(gè)線程檢查器:鎖主要 為了當(dāng)檢測(cè)條件時(shí)保護(hù)數(shù)據(jù)源,執(zhí)行條件引發(fā)的任務(wù);線程檢查器 主要是根據(jù)條件決定是否繼續(xù)運(yùn)行線程孙咪,即線程是否被阻塞堪唐。
它適用于一個(gè)生產(chǎn)消費(fèi)者模型;
看代碼:
- (void)testConditon{
_testCondition = [[NSCondition alloc] init];
//創(chuàng)建生產(chǎn)-消費(fèi)者
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self producer];
});
}
}
- (void)producer{
[_testCondition lock]; // 操作的多線程影響
self.ticketCount = self.ticketCount + 1;
NSLog(@"生產(chǎn)一個(gè) 現(xiàn)有 count %zd",self.ticketCount);
[_testCondition signal]; // 信號(hào)
[_testCondition unlock];
}
- (void)consumer{
[_testCondition lock]; // 操作的多線程影響
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
[_testCondition wait];
}
//注意消費(fèi)行為翎蹈,要在等待條件判斷之后
self.ticketCount -= 1;
NSLog(@"消費(fèi)一個(gè) 還剩 count %zd ",self.ticketCount);
[_testCondition unlock];
}
上面的代碼執(zhí)行流程需要先生產(chǎn)出來淮菠,才能消費(fèi),當(dāng)消費(fèi)多了荤堪,還沒生產(chǎn)完合陵,就需要等待。
那么為了避免多線程的影響澄阳,需要對(duì)生產(chǎn)的時(shí)候進(jìn)行加鎖曙寡,防止它還為生產(chǎn)完,就進(jìn)行消費(fèi)了寇荧,當(dāng)生產(chǎn)完了举庶,發(fā)送一個(gè)信號(hào),再進(jìn)行解鎖揩抡。
那么新增需求户侥,當(dāng)我們需要對(duì)事務(wù)進(jìn)行順序處理時(shí)镀琉,如何使用鎖來處理;那就要介紹一下NSConditionLock
這把條件鎖了蕊唐,
看代碼:
// 信號(hào)量
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1]; // conditoion = 1 內(nèi)部 Condition 匹配
// -[NSConditionLock lockWhenCondition: beforeDate:]
NSLog(@"線程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
sleep(0.1);
NSLog(@"線程 2");
// self.myLock.value = 1;
[conditionLock unlockWithCondition:1]; // _value = 2 -> 1
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"線程 3");
[conditionLock unlock];
});
這把鎖的特殊性在于這把鎖是有一個(gè)條件屋摔,通過設(shè)置信號(hào)量lockWhenCondition
,在執(zhí)行時(shí)會(huì)對(duì)信號(hào)量進(jìn)行匹配替梨,首先執(zhí)行線程3钓试,因?yàn)樗鼪]有設(shè)置信號(hào)量,然后信號(hào)量進(jìn)行匹配副瀑,由于初始化時(shí)設(shè)置了信號(hào)量為2弓熏,因此先執(zhí)行線程2,在里面又設(shè)置了解鎖信號(hào)量為1糠睡,就匹配線程1挽鞠,如果解鎖信號(hào)量與線程1的信號(hào)量不匹配,那么線程1不會(huì)執(zhí)行狈孔。
注意信认,線程3與線程2的順序有可能是不確定的,如果線程3比較耗時(shí)均抽,那么有可能會(huì)先執(zhí)行線程2嫁赏,再執(zhí)行線程1,最后執(zhí)行線程3.
那么關(guān)于其他的一些鎖這邊就不再介紹了油挥。