多線程的鎖與信號(hào)量

多線程中,鎖大部分可以分成兩種,互斥鎖與自旋鎖。

  • 互斥鎖 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);

image.png

優(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ō)替換自旋鎖OSSpinLockos_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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容