鎖
多線程中,鎖大部分可以分成兩種,互斥鎖與自旋鎖。
互斥鎖 Mutex
互斥鎖也稱(chēng)互斥量 ,屬于sleep-waiting類(lèi)型的鎖,當(dāng)線程訪問(wèn)被鎖資源時(shí),調(diào)用者線程會(huì)休眠,此時(shí)cpu可以調(diào)度其他線程工作。直到被鎖資源釋釋放鎖。此時(shí)會(huì)喚醒休眠線程?;コ怄i的加鎖與解鎖操作回設(shè)計(jì)系統(tǒng)線程的調(diào)度與上下文切換。自旋鎖 Spinlock
屬于busy-wait 類(lèi)型的鎖,調(diào)用者線程反復(fù)檢查鎖變量是否可用。由于線程在這一過(guò)程中保持執(zhí)行,因此是一種忙等待。一旦獲取了自旋鎖,線程會(huì)一直保持該鎖,直至顯式釋放自旋鎖。自旋鎖會(huì)比互斥鎖更加消耗CPU,自旋鎖的效率也會(huì)比互斥鎖更高。死鎖
兩個(gè)運(yùn)行單元都在等待對(duì)方停止運(yùn)行,以獲取系統(tǒng)資源,但是沒(méi)有一方提前退出時(shí),就稱(chēng)為死鎖加鎖與解鎖
加鎖其實(shí)就是獲得鎖,獲得這個(gè)這個(gè)資源的訪問(wèn)權(quán)限,解鎖就是釋放鎖,其他線程就可以訪問(wèn)。
iOS的鎖
在iOS中,實(shí)現(xiàn)鎖有多種方式,一般有
- @synchronized 同步代碼塊
@synchronized(self) {//入?yún)elf為所要保護(hù)的對(duì)象,內(nèi)部其實(shí)是一個(gè)遞歸鎖
// task
}
@synchronized可以很方便就給對(duì)象加鎖,不用再額外聲明鎖,手動(dòng)加鎖和釋放鎖。需要保護(hù)的代碼寫(xiě)在block即可,但是效率較慢。
- NSLock 對(duì)象鎖
NSLock *mutexLock = [[NSLock alloc] init];
[mutexLock lock];
//task
[mutexLock unlock];
NSRecursiveLock 遞歸鎖
NSLock在遞歸中容易產(chǎn)生死鎖,使用NSRecursiveLock可以避免這個(gè)問(wèn)題,NSRecursiveLock可以在被同一線程重復(fù)獲取時(shí)不會(huì)產(chǎn)生死鎖。NSConditionLock 條件鎖
滿足預(yù)設(shè)的條件,就可以獲取鎖OSSpinLock 自旋鎖
一般的互斥鎖沒(méi)有拿到鎖之前,線程都會(huì)休眠,而自旋鎖不會(huì)引起調(diào)用者線程休眠,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖。pthread_mutex C語(yǔ)言實(shí)現(xiàn)的互斥鎖
從效率上看,自然是OSSpinLock自旋鎖的效率最高,執(zhí)行耗時(shí)最小,但是OSSpinLock會(huì)出現(xiàn)優(yōu)先級(jí)反轉(zhuǎn)的問(wèn)題,目前OSSpinLock的內(nèi)部實(shí)現(xiàn)已經(jīng)被os_unfair_lock代替。等待os_unfair_lock鎖的其他線程會(huì)處于休眠狀態(tài),而并非忙等。
需要注意的是,iOS中的鎖目前按照功能來(lái)分基本是兩種,自旋鎖和互斥鎖,其他的鎖基本本質(zhì)是互斥鎖,大體都是封裝pthread_mutex而來(lái)。
typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);

優(yōu)先級(jí)反轉(zhuǎn)
在 iOS 中,高優(yōu)先級(jí)high priority線程始終會(huì)在低優(yōu)先級(jí)(low priority )線程前執(zhí)行,一個(gè)線程不會(huì)受到比它更低優(yōu)先級(jí)low priority線程的干擾。具體來(lái)說(shuō),如果一個(gè)低優(yōu)先級(jí)的線程獲得鎖并訪問(wèn)共享資源,這時(shí)一個(gè)高優(yōu)先級(jí)high priority的線程也嘗試獲得這個(gè)鎖,它會(huì)處于 spin lock 的忙等狀態(tài)從而占用大量 CPU。此時(shí)低優(yōu)先級(jí)線程無(wú)法與高優(yōu)先級(jí)線程爭(zhēng)奪 CPU 時(shí)間片,從而導(dǎo)致任務(wù)遲遲完不成、無(wú)法釋放 lock。除非開(kāi)發(fā)者能保證訪問(wèn)鎖的線程全部都處于同一優(yōu)先級(jí),否則 iOS 系統(tǒng)中所有類(lèi)型的自旋鎖都不能再使用了。
CPU調(diào)度
一般來(lái)說(shuō)線程是程序執(zhí)行的最小單元,一個(gè)線程包括:獨(dú)有ID,程序計(jì)數(shù)器,寄存器集合,堆棧。同一進(jìn)程可以有多個(gè)線程,它們共享進(jìn)程的全局變量和堆數(shù)據(jù)。CPU的核心在同一個(gè)時(shí)刻只能執(zhí)行一條線程,如果要執(zhí)行的線程超過(guò)CPU的核心數(shù)時(shí),就需要線程調(diào)度,簡(jiǎn)單來(lái)說(shuō)就是:一個(gè) CPU 核心輪流讓各個(gè)線程分別執(zhí)行一段時(shí)間。上一點(diǎn)所述,在線程調(diào)度里面,高優(yōu)先級(jí)的線程會(huì)優(yōu)先獲得CPU時(shí)間片。線程調(diào)度還有其他算法,如FIFO,先排隊(duì)的線程獲得運(yùn)行的CPU時(shí)間片。CPU調(diào)度使得等待的線程可以運(yùn)行,這樣的切換同時(shí)也會(huì)伴隨著上下文的切換(寄存器數(shù)據(jù)、棧等),過(guò)多的上下文切換會(huì)帶來(lái)資源開(kāi)銷(xiāo)。
公平鎖與非公平鎖
上述所說(shuō)替換自旋鎖OSSpinLock的os_unfair_lock,其實(shí)是非公平鎖,平時(shí)使用的鎖基本都是公平鎖,這一類(lèi)鎖有著FIFO的特性,
多個(gè)線程情況下排隊(duì),先到先獲得鎖。如果進(jìn)入等待的順序?yàn)?2345,則最后等待結(jié)束被執(zhí)行的順序也是12345。但是如果使用的是非公平鎖,前面的任務(wù)馬上要執(zhí)行完畢,若釋放鎖的時(shí)候,正好一個(gè)新的線程6來(lái)訪問(wèn)資源,而此時(shí)位于隊(duì)列頭的線程1還沒(méi)有被喚醒(因?yàn)榫€程上下文切換是需要不少開(kāi)銷(xiāo)的),此時(shí)后來(lái)的線程6則優(yōu)先獲得鎖,成功打破公平,成為非公平鎖。但是如果線程6來(lái)訪問(wèn)時(shí),鎖不是剛好釋放,或者線程1已經(jīng)被喚醒,線程6還是得進(jìn)入線程隊(duì)列中休眠等待CPU的喚醒執(zhí)行。
信號(hào)量(Semaphore)
信號(hào)量是一個(gè)同步對(duì)象,用于保持在0至指定最大值之間的一個(gè)計(jì)數(shù)值。當(dāng)線程完成一次對(duì)該semaphore對(duì)象的等待(wait)時(shí),該計(jì)數(shù)值(- 1)加鎖;當(dāng)線程完成一次對(duì)semaphore對(duì)象的釋放(release)時(shí),計(jì)數(shù)值(+ 1)釋放鎖。簡(jiǎn)單來(lái)說(shuō),信號(hào)量為0的時(shí)候,線程會(huì)阻塞,一直等待該信號(hào)量對(duì)象的計(jì)數(shù)值變成大于0的狀態(tài)。
如在OC中,
關(guān)于信號(hào)量主要有三個(gè)函數(shù):
-
dispatch_semaphore_create(long value);
創(chuàng)建信號(hào)量,參數(shù)為設(shè)置信號(hào)量的初始值 -
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
發(fā)送當(dāng)前信號(hào)量,參數(shù)為當(dāng)前創(chuàng)建的信號(hào)量 -
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
等待信號(hào)量,第一個(gè)為當(dāng)前等待的信號(hào)量,第二個(gè)參數(shù)為超時(shí)時(shí)間。當(dāng)?shù)却龝r(shí)間超過(guò)超時(shí)時(shí)間就不會(huì)繼續(xù)等待了。
信號(hào)量是一個(gè)整型值,在創(chuàng)建的時(shí)候會(huì)有一個(gè)初始值。當(dāng)執(zhí)行dispatch_semaphore_signal發(fā)送信號(hào)的時(shí)候信號(hào)量會(huì)加1,dispatch_semaphore_wait在信號(hào)量小于或等于0的時(shí)候會(huì)一直等待,直到超時(shí),并且會(huì)阻塞該線程,當(dāng)信號(hào)量大于0時(shí)會(huì)繼續(xù)執(zhí)行并對(duì)信號(hào)量執(zhí)行減1操作。
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t lock = dispatch_semaphore_create(0);//信號(hào)量初始為0
dispatch_async(queue, ^{
// task1
dispatch_semaphore_signal(lock);// 使信號(hào)量+1并返回
});
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);//堵塞當(dāng)前線程,等待task1結(jié)束,信號(hào)量加1,釋放鎖。
// task2
//只有當(dāng)task1執(zhí)行完,信號(hào)量+1之后,才會(huì)執(zhí)行這里
數(shù)值為N的信號(hào)量允許N個(gè)線程并發(fā)訪問(wèn)。
如果信號(hào)量是一個(gè)任意的整數(shù),通常被稱(chēng)為計(jì)數(shù)信號(hào)量(Counting semaphore),或一般信號(hào)量(general semaphore);如果信號(hào)量只有二進(jìn)制的0或1,稱(chēng)為二進(jìn)制信號(hào)量(binary semaphore)。在linux系統(tǒng)中,二進(jìn)制信號(hào)量(binary semaphore)又稱(chēng)互斥量(Mutex)。
所以說(shuō),互斥量和信號(hào)量本質(zhì)上一樣,都是用來(lái)表示對(duì)資源的訪問(wèn)權(quán),但是互斥量表示資源某個(gè)時(shí)刻最多只能被一個(gè)線程占用,也就是資源計(jì)數(shù)最多是1,而信號(hào)量的資源計(jì)數(shù)可以超過(guò)1,即同時(shí)被多個(gè)線程占用。
兩者對(duì)比的話,簡(jiǎn)單來(lái)說(shuō),
- 互斥量就是N個(gè)線程,爭(zhēng)奪一把鎖,
- 信號(hào)量就是N個(gè)線程,爭(zhēng)奪M把鎖,當(dāng)信號(hào)量的計(jì)數(shù)值只有0、1(二進(jìn)制信號(hào)量),那么M = 1,這時(shí)候,和互斥量的性質(zhì)是一樣的。
信號(hào)量死鎖問(wèn)題
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self testSemaphore];
}
/// 主線程運(yùn)行
- (void)testSemaphore
{
NSLog(@"1");
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_main_queue(), ^{//主線程
sleep(1);
NSLog(@"2");
dispatch_semaphore_signal(semaphore);
});
NSLog(@"3");
//堵塞當(dāng)前線程
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"4");
}
這時(shí)候輸出
2021-05-15 23:28:12.156395+0800 TestProject[10564:1787212] 1
2021-05-15 23:28:12.156641+0800 TestProject[10564:1787212] 3
就死鎖了,兩個(gè)運(yùn)行單元出現(xiàn)互相等待的情況。因?yàn)閐ispatch_semaphore_wait卡主了主線程,而dispatch_async(dispatch_get_main_queue()又是在主線程中運(yùn)行,需要等里面的block運(yùn)行結(jié)束,信號(hào)量+1,釋放鎖后,wait才會(huì)結(jié)束。解決這個(gè)問(wèn)題,可以把避免在主線程中執(zhí)行Block,或者wait的時(shí)間可以手動(dòng)設(shè)置短一些。
參考文章
https://zh.wikipedia.org/wiki/%E4%BF%A1%E8%99%9F%E6%A8%99
https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/