多線程需要一種互斥的機(jī)制來(lái)訪問(wèn)共享資源。
一浇冰、 互斥鎖
互斥鎖的意思是某一時(shí)刻只允許一個(gè)線程訪問(wèn)某一資源曾掂。為了保證這一點(diǎn),每個(gè)想要訪問(wèn)共享資源的線程顽腾,需要首先獲得一個(gè)共享資源的互斥鎖近零,一旦某個(gè)線程對(duì)共享資源完成了訪問(wèn),就釋放掉這個(gè)互斥鎖,這樣別的線程就有機(jī)會(huì)獲取互斥鎖久信,然后訪問(wèn)該共享資源了猪瞬。
一般情況下,一個(gè)線程只能申請(qǐng)一次鎖入篮,也只能在獲得鎖的情況下才能釋放鎖,多次申請(qǐng)鎖或釋放未獲得的鎖都會(huì)導(dǎo)致崩潰幌甘。假設(shè)在已經(jīng)獲得鎖的情況下再次申請(qǐng)鎖潮售,線程會(huì)因?yàn)榈却i的釋放而進(jìn)入睡眠狀態(tài),因此就不可能再釋放鎖锅风,從而導(dǎo)致死鎖酥诽。
互斥鎖的實(shí)現(xiàn)原理與信號(hào)量非常相似,不是使用忙等皱埠,而是阻塞線程并睡眠肮帐,需要進(jìn)行上下文切換。
1. pthread_mutex
由于 pthread_mutex 有多種類型边器,可以支持遞歸鎖等训枢,因此在申請(qǐng)加鎖時(shí),需要對(duì)鎖的類型加以判斷忘巧,這也就是為什么它和信號(hào)量的實(shí)現(xiàn)類似恒界,但效率略低的原因。
如果已經(jīng)得到鎖砚嘴,再次申請(qǐng)鎖十酣,會(huì)導(dǎo)致死鎖。然而這種情況經(jīng)常會(huì)發(fā)生际长,比如某個(gè)函數(shù)申請(qǐng)了鎖耸采,在臨界區(qū)內(nèi)又遞歸調(diào)用了自己。辛運(yùn)的是 pthread_mutex 支持遞歸鎖工育,也就是允許一個(gè)線程遞歸的申請(qǐng)鎖虾宇,只要把 attr 的類型改成 PTHREAD_MUTEX_RECURSIVE 即可。
2. NSLock
NSLock只是在內(nèi)部封裝了一個(gè)pthread_mutex翅娶,屬性為PTHREAD_MUTEX_ERRORCHECK文留,它會(huì)損失一定性能換來(lái)錯(cuò)誤提示。這里使用宏定義的原因是竭沫,OC 內(nèi)部還有其他幾種鎖燥翅,他們的 lock 方法都是一模一樣,僅僅是內(nèi)部pthread_mutex
互斥鎖的類型不同蜕提。通過(guò)宏定義森书,可以簡(jiǎn)化方法的定義。
NSLock比pthread_mutex略慢的原因在于它需要經(jīng)過(guò)方法調(diào)用,同時(shí)由于緩存的存在凛膏,多次方法調(diào)用不會(huì)對(duì)性能產(chǎn)生太大的影響杨名。
//設(shè)置票的數(shù)量為5
_tickets = 5;
//創(chuàng)建鎖
_mutexLock = [[NSLock alloc] init];
//線程1
dispatch_async(self.concurrentQueue, ^{
[self saleTickets];
});
//線程2
dispatch_async(self.concurrentQueue, ^{
[self saleTickets];
});
- (void)saleTickets
{
while (1) {
[NSThread sleepForTimeInterval:1];
//加鎖
[_mutexLock lock];
if (_tickets > 0) {
_tickets--;
NSLog(@"剩余票數(shù)= %ld, Thread:%@",_tickets,[NSThread currentThread]);
} else {
NSLog(@"票賣完了 Thread:%@",[NSThread currentThread]);
break;
}
//解鎖
[_mutexLock unlock];
}
}
性能與使用場(chǎng)景
pthread_mutex是pthread經(jīng)典的基于互斥量機(jī)制的同步鎖,特性猖毫、性能以及穩(wěn)定各方面都已被大量項(xiàng)目所驗(yàn)證台谍,也是比較推薦作為常規(guī)同步鎖首選
二、自旋鎖OSSpinLock
上述文章中已經(jīng)介紹了 OSSpinLock 不再安全吁断,主要原因發(fā)生在低優(yōu)先級(jí)線程拿到鎖時(shí)趁蕊,高優(yōu)先級(jí)線程進(jìn)入忙等(busy-wait)狀態(tài),一直循環(huán)仔役,消耗大量 CPU 時(shí)間掷伙,從而導(dǎo)致低優(yōu)先級(jí)線程拿不到 CPU 時(shí)間,也就無(wú)法完成任務(wù)并釋放鎖又兵。這種問(wèn)題被稱為優(yōu)先級(jí)反轉(zhuǎn)任柜。
原理,通過(guò)一個(gè)全局變量和申請(qǐng)鎖的原子操作沛厨。
然而在多處理器的情況下宙地,能夠被多個(gè)處理器同時(shí)執(zhí)行的操作任然算不上原子操作。因此俄烁,真正的原子操作必須由硬件提供支持绸栅,比如 x86 平臺(tái)上如果在指令前面加上 “LOCK” 前綴,對(duì)應(yīng)的機(jī)器碼在執(zhí)行時(shí)會(huì)把總線鎖住页屠,使得其他 CPU不能再執(zhí)行相同操作粹胯,從而從硬件層面確保了操作的原子性。
這些非常底層的概念無(wú)需完全掌握辰企,我們只要知道上述申請(qǐng)鎖的過(guò)程风纠,可以用一個(gè)原子性操作 test_and_set 來(lái)完成。
實(shí)際使用:
#import <libkern/OSAtomic.h>
@interface ViewController ()
{
OSSpinLock spinlock;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.number = 10;
spinlock = OS_SPINLOCK_INIT;
}
- (IBAction)test:(id)sender {
for (int i = 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self sellTicket];
});
}
}
- (void)sellTicket {
OSSpinLockLock(&spinlock);
if (self.number > 0) {
self.number--;
NSLog(@"%@還剩%ld張票",[NSThread currentThread],self.number);
}
OSSpinLockUnlock(&spinlock);
}
@end
使用場(chǎng)景
如果臨界區(qū)的執(zhí)行時(shí)間過(guò)長(zhǎng)牢贸,使用自旋鎖不是個(gè)好主意竹观,自旋鎖適合短時(shí)間的操作,加鎖性能最快潜索,但不能使用不同優(yōu)先級(jí)臭增。
三、信號(hào)量
不是使用忙等竹习,而是阻塞線程并睡眠誊抛,需要進(jìn)行上下文切換。
缺點(diǎn)
在時(shí)間較短的操作整陌,沒(méi)有自旋鎖高效拗窃,會(huì)有上下文切換的成本瞎领。
優(yōu)點(diǎn)
效率高。
四随夸、條件鎖
1. NSCondition
NSCondition 其實(shí)是封裝了一個(gè)互斥鎖和條件變量九默。NSCondition 的底層是通過(guò)條件變量(condition variable) pthread_cond_t 來(lái)實(shí)現(xiàn)的。條件變量有點(diǎn)像信號(hào)量宾毒,提供了線程阻塞與信號(hào)機(jī)制驼修,因此可以用來(lái)阻塞某個(gè)線程,并等待某個(gè)數(shù)據(jù)就緒诈铛,隨后喚醒線程邪锌。它僅僅是控制了線程的執(zhí)行順序。
互斥鎖提供線程安全癌瘾,條件變量提供線程阻塞與信號(hào)機(jī)制。
它的基本用法和NSLock一樣饵溅,這里說(shuō)一下NSCondition的特殊用法妨退。
NSCondition提供更高級(jí)的用法,方法如下:
- (void)wait; //阻塞當(dāng)前線程 直到等待喚醒
- (BOOL)waitUntilDate:(NSDate *)limit; //阻塞當(dāng)前線程到一定時(shí)間 之后自動(dòng)喚醒
- (void)signal; //喚醒一條阻塞線程
- (void)broadcast; //喚醒所有阻塞線程
2. NSConditionLock
借助 NSCondition 來(lái)實(shí)現(xiàn)蜕企,它的本質(zhì)就是一個(gè)生產(chǎn)者-消費(fèi)者模型咬荷。“條件被滿足”可以理解為生產(chǎn)者提供了新的內(nèi)容轻掩。NSConditionLock 的內(nèi)部持有一個(gè) NSCondition 對(duì)象幸乒,以及 _condition_value 屬性,在初始化時(shí)就會(huì)對(duì)這個(gè)屬性進(jìn)行賦值:
// 簡(jiǎn)化版代碼
- (id) initWithCondition: (NSInteger)value {
if (nil != (self = [super init])) {
_condition = [NSCondition new]
_condition_value = value;
}
return self;
}
它的 lockWhenCondition 方法其實(shí)就是消費(fèi)者方法:
- (void) lockWhenCondition: (NSInteger)value {
[_condition lock];
while (value != _condition_value) {
[_condition wait];
}
}
對(duì)應(yīng)的 unlockWhenCondition 方法則是生產(chǎn)者唇牧,使用了 broadcast 方法通知了所有的消費(fèi)者:
- (void) unlockWithCondition: (NSInteger)value {
_condition_value = value;
[_condition broadcast];
[_condition unlock];
}
具體使用:
//主線程中
NSConditionLock *theLock = [[NSConditionLock alloc] init];
//線程1
dispatch_async(self.concurrentQueue, ^{
for (int i=0;i<=3;i++)
{
[theLock lock];
NSLog(@"thread1:%d",i);
sleep(1);
[theLock unlockWithCondition:i];
}
});
//線程2
dispatch_async(self.concurrentQueue, ^{
[theLock lockWhenCondition:2];
NSLog(@"thread2");
[theLock unlock];
});
五罕扎、遞歸鎖NSRecursiveLock
上文已經(jīng)說(shuō)過(guò),遞歸鎖也是通過(guò) pthread_mutex_lock
函數(shù)來(lái)實(shí)現(xiàn)丐重,在函數(shù)內(nèi)部會(huì)判斷鎖的類型腔召,如果顯示是遞歸鎖,就允許遞歸調(diào)用扮惦,僅僅將一個(gè)計(jì)數(shù)器加一臀蛛,鎖的釋放過(guò)程也是同理。
NSRecursiveLock
與 NSLock
的區(qū)別在于內(nèi)部封裝的 pthread_mutex_t
對(duì)象的類型不同崖蜜,前者的類型為 PTHREAD_MUTEX_RECURSIVE
浊仆。
多次調(diào)用不會(huì)阻塞已獲取該鎖的線程,不會(huì)死鎖豫领。
實(shí)際使用:
// 實(shí)例類person
Person *person = [[Person alloc] init];
// 創(chuàng)建鎖對(duì)象
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
// 創(chuàng)建遞歸方法
static void (^testCode)(int);
testCode = ^(int value) {
[theLock tryLock];
if (value > 0)
{
[person personA];
[NSThread sleepForTimeInterval:1];
testCode(value - 1);
}
[theLock unlock];
};
//線程A
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
testCode(5);
});
//線程B
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[theLock lock];
[person personB];
[theLock unlock];
});
六抡柿、@synchronized
這其實(shí)是一個(gè) OC 層面的鎖, 主要是通過(guò)犧牲性能換來(lái)語(yǔ)法上的簡(jiǎn)潔與可讀氏堤。
我們知道 @synchronized 后面需要緊跟一個(gè) OC 對(duì)象沙绝,它實(shí)際上是把這個(gè)對(duì)象當(dāng)做鎖來(lái)使用搏明。你調(diào)用 sychronized 的每個(gè)對(duì)象,Objective-C runtime 都會(huì)為其分配一個(gè)遞歸鎖并存儲(chǔ)在哈希表中闪檬。OC 在底層使用了一個(gè)互斥鎖的數(shù)組(你可以理解為鎖池)星著,通過(guò)對(duì)對(duì)象地址哈希值來(lái)得到對(duì)應(yīng)的互斥鎖。
若是在self對(duì)象上頻繁加鎖粗悯,那么程序可能要等另一段與此無(wú)關(guān)的代碼執(zhí)行完畢虚循,才能繼續(xù)執(zhí)行當(dāng)前代碼,這樣做其實(shí)并沒(méi)有必要样傍。
使用場(chǎng)景:創(chuàng)建單例時(shí)使用横缔。
綜合上述分析與討論,總結(jié)有以下幾點(diǎn)原則:
1衫哥、總的來(lái)看茎刚,推薦pthread_mutex作為實(shí)際項(xiàng)目的首選方案;
2撤逢、對(duì)于耗時(shí)較大又易沖突的讀操作膛锭,可以使用讀寫鎖代替pthread_mutex;
3蚊荣、如果確認(rèn)僅有set/get的訪問(wèn)操作初狰,可以選用原子操作屬性;
4互例、對(duì)于性能要求苛刻奢入,可以考慮使用OSSpinLock,需要確保加鎖片段的耗時(shí)足夠邢边丁腥光;
5、條件鎖基本上使用面向?qū)ο蟮腘SCondition和NSConditionLock即可糊秆;
6柴我、@synchronized則適用于低頻場(chǎng)景如初始化或者緊急修復(fù)使用;
1.自旋鎖:OSSpinLock 在ios中已經(jīng)不是線程安全的了扩然,如果共享數(shù)據(jù)已經(jīng)有其他線程加鎖了艘儒,線程會(huì)以死循環(huán)的方式等待鎖,一旦被訪問(wèn)的資源被解鎖夫偶,則等待資源的線程會(huì)立即執(zhí)行界睁。(效率最高,如果一直等不到鎖會(huì)較占用cpu資源)
2.信號(hào)量:dispatch_semaphore是gcd中通過(guò)信號(hào)量來(lái)實(shí)現(xiàn)共享數(shù)據(jù)的數(shù)據(jù)安全兵拢。(效率第二)
3.互斥鎖:pthread_mutex 翻斟,nslock ,synchronized都是互斥鎖说铃。如果共享數(shù)據(jù)已經(jīng)有其他線程加鎖了访惜,線程會(huì)進(jìn)入休眠狀態(tài)等待鎖嘹履。一旦被訪問(wèn)的資源被解鎖,則等待資源的線程會(huì)被喚醒债热。(synchronized效率最低)
4.遞歸鎖:pthread_mutex(recursive)與NSRecursiveLock 砾嫉, 多次調(diào)用不會(huì)阻塞已獲取該鎖的線程。
5.條件鎖:nsconditionlock 滿足一定的條件的加鎖和解鎖窒篱,可以實(shí)現(xiàn)依賴關(guān)系焕刮。nscondition條件鎖,也是通過(guò)信號(hào)來(lái)解鎖墙杯,主要用來(lái)實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模式配并。
七、我們平時(shí)使用的:
1. @synchronized高镐,一般用在創(chuàng)建單例的時(shí)候溉旋。
2. atomic修飾屬性的關(guān)鍵字,它不是絕對(duì)安全的嫉髓。
3. 一般使用NSLock即可低滩,但是如果方法會(huì)有遞歸調(diào)用則會(huì)死鎖
,這時(shí)我們使用遞歸鎖:
4. OSSpinLock自旋鎖岩喷,輪詢的方式,用于輕量級(jí)的數(shù)據(jù)操作+1/-1监憎。
5. 信號(hào)量:dispatch_semaphore是gcd中通過(guò)信號(hào)量來(lái)實(shí)現(xiàn)共享數(shù)據(jù)的數(shù)據(jù)安全纱意。(效率第二)
資料:
[iOS]深入理解并發(fā)--鎖
深入理解 iOS 開(kāi)發(fā)中的鎖
iOS開(kāi)發(fā)-多線程開(kāi)發(fā)之線程安全篇
iOS 中幾種常用的鎖總結(jié)
iOS中的5種鎖