OC底層原理 - 23 iOS中的鎖

引言

鎖是開(kāi)發(fā)中最常用的同步工具,通過(guò)鎖來(lái)實(shí)現(xiàn)對(duì)臨界資源的訪問(wèn)控制烹看,從而使目標(biāo)代碼段同一時(shí)間只會(huì)被一個(gè)線程執(zhí)行国拇。這是一種以犧牲性能為代價(jià)的方法。

鎖的實(shí)現(xiàn)依賴于原子操作惯殊,不同的處理器(intel酱吝、arm),不同的架構(gòu)(單核靠胜、多核)實(shí)現(xiàn)原子操作的方式不一樣掉瞳。有的是通過(guò)加鎖封鎖總線,有的是做成單指令浪漠,有的是依據(jù)標(biāo)志位陕习,有的是依據(jù)CPU相關(guān)的指令對(duì),總之址愿,不同的機(jī)制可以實(shí)現(xiàn)原子操作该镣。

原子操作,就像原子一樣不可再分割的操作响谓,即:一個(gè)操作(有可能包含多個(gè)子操作)只要開(kāi)始執(zhí)行损合,在執(zhí)行完畢前,不會(huì)被其它操作或者指令中斷娘纷。原子操作解決了多線程不安全問(wèn)題中的原子性問(wèn)題嫁审。如果沒(méi)有原子操作的話,操作可能會(huì)因?yàn)橹袛喈惓5雀鞣N原因引起數(shù)據(jù)狀態(tài)的不一致赖晶,從而影響到程序的正確性律适。

iOS中的atomic屬性修飾符的語(yǔ)義就是原子操作辐烂。被atomic所修飾的屬性,確保了setter和getter的原子性捂贿,這使得setter和getter這兩個(gè)方法是線程安全的纠修,但是對(duì)于整個(gè)對(duì)象來(lái)說(shuō),不一定是線程安全的厂僧。并且atomic比nonatomic開(kāi)銷要大很多扣草,所以一般考慮到性能時(shí),會(huì)將屬性修飾符設(shè)置為nonatomic颜屠。

雖然鎖是同步兩個(gè)線程的有效辦法辰妙,但是獲取鎖是一個(gè)相對(duì)昂貴的操作,即使在無(wú)爭(zhēng)用的情況下汽纤,也是如此上岗。相比之下,許多原子操作只需要花費(fèi)一小部分時(shí)間就可以完成蕴坪,并且可以像鎖一樣有效。

使用鎖可以保證多線程操作共享數(shù)據(jù)時(shí)的安全問(wèn)題敬锐,卻也降低了程序的執(zhí)行效率背传。鎖的這種機(jī)制無(wú)法徹底避免以下幾點(diǎn)問(wèn)題:
① 鎖引起的線程阻塞,對(duì)于沒(méi)有能占用到鎖的線程或者進(jìn)程將會(huì)一直等待鎖的占有者釋放資源后才能繼續(xù)台夺。
② 申請(qǐng)和釋放鎖的操作增加了很多訪問(wèn)共享資源的消耗径玖。
③ 鎖不能很好的避免編程開(kāi)發(fā)者設(shè)計(jì)實(shí)現(xiàn)的程序出現(xiàn)死鎖或者活鎖的可能。
④ 優(yōu)先級(jí)反轉(zhuǎn)和鎖護(hù)送怪現(xiàn)象颤介。
⑤ 難以調(diào)試梳星。

鎖的分類

鎖的分類多種多樣,根據(jù)線程的狀態(tài)可以分為:互斥鎖自旋鎖滚朵。

互斥鎖:互斥鎖充當(dāng)資源周圍的保護(hù)屏障冤灾,如果多個(gè)線程競(jìng)爭(zhēng)同一個(gè)互斥鎖,每次只允許一個(gè)線程訪問(wèn)辕近。如果一個(gè)互斥鎖正在使用中韵吨,另一個(gè)線程試圖獲取它,該線程就會(huì)阻塞归粉,進(jìn)入睡眠狀態(tài),直到該互斥鎖被它的持有者釋放后再將其喚醒漏峰。注意:互斥鎖阻塞的線程處于休眠狀態(tài)糠悼。

自旋鎖:如果一個(gè)自旋鎖正在使用中,另一個(gè)線程試圖獲取它時(shí)浅乔,該線程不會(huì)進(jìn)入睡眠狀態(tài)倔喂,而是反復(fù)輪詢其鎖條件,直到該條件為真。這適用于競(jìng)爭(zhēng)預(yù)期較低的情況滴劲。注意:自旋鎖阻塞的線程處于忙等狀態(tài)

使線程進(jìn)入睡眠狀態(tài)攻晒,主動(dòng)讓出時(shí)間片并不代表效率高,因?yàn)椴僮飨到y(tǒng)切換到另一個(gè)線程上下文時(shí)班挖,通常需要10ms鲁捏,而且需要切換兩次。因此萧芙,如果鎖的預(yù)期等待時(shí)間很短给梅,輪詢通常比線程休眠更有效。

iOS中的鎖

轉(zhuǎn)載自iOS多線程編程(七) 同步機(jī)制與鎖

pthread_mutex 互斥鎖

互斥鎖是一種用來(lái)防止多個(gè)線程同一時(shí)刻對(duì)共享資源進(jìn)行訪問(wèn)的信號(hào)量双揪,它的原子性確保了如果一個(gè)線程鎖定了一個(gè)互斥量动羽,將沒(méi)有其他線程在同一時(shí)間可以鎖定這個(gè)互斥量。它的唯一性確保了只有它解鎖了這個(gè)互斥量渔期,其他線程才可以對(duì)其進(jìn)行鎖定运吓。當(dāng)一個(gè)線程鎖定一個(gè)資源的時(shí)候,其他對(duì)該資源進(jìn)行訪問(wèn)的線程將會(huì)被掛起疯趟,直到該線程解鎖了互斥量拘哨,其他線程才會(huì)被喚醒,進(jìn)一步才能鎖定該資源進(jìn)行操作信峻。

pthread_mutex是POSIX提供的互斥鎖倦青,基于C語(yǔ)言實(shí)現(xiàn),可跨平臺(tái)盹舞〔洌基本上OC層面的互斥鎖都是基于pthread_mutex實(shí)現(xiàn)的。主要的函數(shù)如下:

// 宏定義踢步。用于靜態(tài)的mutex的初始化癣亚,采用默認(rèn)的attr。
PTHREAD_MUTEX_INITIALIZER 
// 用于動(dòng)態(tài)的mutex的初始化贾虽,第二個(gè)參數(shù)為mutex的屬性attr
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 
// 請(qǐng)求獲得鎖逃糟,如果當(dāng)前mutex未被持有,則加鎖成功蓬豁;
// 如果當(dāng)前mutex已被持有绰咽,那么請(qǐng)求加鎖線程不會(huì)獲得成功,并阻塞線程地粪,直到mutex被釋放
int pthread_mutex_lock(pthread_mutex_t *mutex); 
// 釋放鎖
int pthread_mutex_unlock(pthread_mutex_t *mutex); 
// 嘗試獲得鎖取募,如果當(dāng)前mutex已經(jīng)被持有或者不可用,這個(gè)函數(shù)就直接return蟆技,不會(huì)阻塞當(dāng)前線程
int pthread_mutex_trylock(pthread_mutex_t *mutex); 
// 銷毀mutex鎖玩敏,并且釋放所有它所占有的資源
int pthread_mutex_destroy(pthread_mutex_t *mutex); 

使用pthread_mutex的主要過(guò)程為:

  • ① 創(chuàng)建pthread_mutex斗忌;
  • ② 使用pthread_mutex_lock加鎖,使用pthread_mutex_unlock解鎖旺聚;
  • ③ 銷毀pthread_mutex织阳;

創(chuàng)建pthread_mutex:

初始化pthread_mutex有兩種方式,一種是通過(guò)宏定義(PTHREAD_MUTEX_INITIALIZER)獲得默認(rèn)的互斥鎖砰粹,另一種是通過(guò)函數(shù)(pthread_mutex_init )創(chuàng)建鎖唧躲。如果不需要自定義pthread_mutex的屬性信息,使用宏定義的方式更快速便捷碱璃。

使用pthread_mutex_lock加鎖與pthread_mutex_unlock解鎖

pthread_mutex(互斥鎖)利用排他性來(lái)保證線程安全弄痹,在同一時(shí)刻只允許一個(gè)線程獲得鎖。如果一個(gè)線程已經(jīng)獲得互斥鎖嵌器,另一個(gè)線程就無(wú)法訪問(wèn)肛真,直到鎖的持有者正確的釋放了互斥鎖,另一個(gè)線程才有機(jī)會(huì)獲得鎖爽航。

- (void)pthread_mutexDemo {
  
    // 創(chuàng)建mutex
    __block pthread_mutex_t mutex = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
    // 線程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        pthread_mutex_lock(&mutex);
        NSLog(@"執(zhí)行任務(wù)A---%@",[NSThread currentThread]);
        sleep(5);
        NSLog(@"任務(wù)A執(zhí)行完畢---%@",[NSThread currentThread]);
        pthread_mutex_unlock(&mutex);
    });
    // 線程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);// 讓線程1的任務(wù)先執(zhí)行
        pthread_mutex_lock(&mutex);
        NSLog(@"執(zhí)行任務(wù)B---%@",[NSThread currentThread]);
        pthread_mutex_unlock(&mutex);
    });
    
    // 銷毀mutex:確保mutex使用完畢再銷毀
//    pthread_mutex_destroy(&mutex);
}

// 打印結(jié)果:
2021-02-27 21:00:14.034887+0800 lockDemo[83241:6444414] 執(zhí)行任務(wù)A---<NSThread: 0x600000fad000>{number = 5, name = (null)}
2021-02-27 21:00:19.039718+0800 lockDemo[83241:6444414] 任務(wù)A執(zhí)行完畢---<NSThread: 0x600000fad000>{number = 5, name = (null)}
2021-02-27 21:00:19.040232+0800 lockDemo[83241:6444416] 執(zhí)行任務(wù)B---<NSThread: 0x600000faee80>{number = 3, name = (null)}

本例中蚓让,線程1先獲得互斥鎖,盡管線程2的異步任務(wù)在sleep(1)后就可執(zhí)行岳掐,但此時(shí)線程1已持有互斥鎖凭疮,所以再次遇到pthread_mutex_lock(&mutex)時(shí),必須等待串述,此時(shí)線程2處于阻塞態(tài),直到 sleep(5)后線程1釋放互斥鎖寞肖,線程2才被喚醒繼續(xù)執(zhí)行任務(wù)纲酗。

使用pthread_mutex_trylock

除了pthread_mutex_lock函數(shù)外,pthread_mutex還提供了pthread_mutex_trylock 函數(shù)新蟆,與pthread_mutex_lock不同的是觅赊,使用pthread_mutex_trylock 函數(shù)來(lái)申請(qǐng)加鎖,不管是否能獲得鎖都立即返回琼稻,并不阻塞線程吮螺。如果申請(qǐng)失敗則返回錯(cuò)誤:EBUSY(鎖尚未解除)或者EINVAL(鎖變量不可用)。一旦在trylock的時(shí)候有錯(cuò)誤返回帕翻,那就把前面已經(jīng)拿到的鎖全部釋放鸠补,然后過(guò)一段時(shí)間再來(lái)一遍。

如果將上例中線程2的pthread_mutex_lock(&mutex)操作嘀掸,換成pthread_mutex_trylock(&mutex)紫岩。則結(jié)果為

2021-02-27 21:04:54.976015+0800 lockDemo[62208:9380951] 執(zhí)行任務(wù)A---<NSThread: 0x6000017de040>{number = 6, name = (null)}
2021-02-27 21:04:55.977173+0800 lockDemo[62208:9380952] 執(zhí)行任務(wù)B---<NSThread: 0x6000017a5980>{number = 4, name = (null)}
2021-02-27 21:04:59.980902+0800 lockDemo[62208:9380951] 任務(wù)A執(zhí)行完畢---<NSThread: 0x6000017de040>{number = 6, name = (null)}

注意事項(xiàng)

  • 避免多次申請(qǐng)鎖或釋放未獲得的鎖

使用pthread_mutex時(shí),pthread_mutex_lock與pthread_mutex_unlock要成對(duì)使用睬塌,一般情況下泉蝌,一個(gè)線程只能申請(qǐng)一次鎖歇万,也只能在獲得鎖的情況下才能釋放鎖,多次申請(qǐng)鎖或釋放未獲得的鎖都會(huì)導(dǎo)致異常勋陪。一定要確保在正確的時(shí)機(jī)獲得鎖和釋放鎖贪磺。

  • 避免阻塞

假設(shè)在已經(jīng)獲得鎖的情況下再次申請(qǐng)鎖,線程會(huì)因?yàn)榈却i的釋放而進(jìn)入睡眠狀態(tài)诅愚,同時(shí)也不可能釋放鎖寒锚。

  • 避免死鎖

如果兩個(gè)線程存在互相等待釋放鎖的情況,也會(huì)導(dǎo)致死鎖的發(fā)生呻粹。

  • 記得pthread_mutex_destroy銷毀鎖壕曼,但要確保pthread_mutex已使用完畢。

pthread_mutex(recursive) 遞歸鎖

在實(shí)際開(kāi)發(fā)中等浊,有可能存在這樣的需求腮郊,遞歸調(diào)用或需要重復(fù)的獲得鎖嗓节。這種情況下两疚,如果使用pthread_mutex(互斥鎖)就會(huì)阻塞線程阀溶,任務(wù)也就無(wú)法繼續(xù)執(zhí)行窖铡。這就需要使用遞歸鎖來(lái)解決問(wèn)題了液走。

遞歸鎖是互斥鎖的變體液南。遞歸鎖允許單個(gè)線程在釋放鎖之前多次獲取該鎖(可重入浪讳,保存了鎖的次數(shù)信息)吸耿。而不會(huì)阻塞當(dāng)前線程制妄,其他線程仍然處于阻塞狀態(tài)掸绞,直到鎖的持有者以獲得鎖的相同次數(shù)釋放鎖。

遞歸鎖主要在遞歸迭代期間使用耕捞,也可以在多個(gè)方法分別需要獲得鎖的情況下使用衔掸。

遞歸鎖的使用:

pthread_mutex維護(hù)了以下幾種鎖類型:

/*
 * Mutex type attributes
 */
#define PTHREAD_MUTEX_NORMAL        0     // 普通互斥鎖
#define PTHREAD_MUTEX_ERRORCHECK    1     // 檢查鎖
#define PTHREAD_MUTEX_RECURSIVE     2     // 遞歸鎖
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL

PTHREAD_MUTEX_NORMAL是默認(rèn)屬性的互斥鎖;與PTHREAD_MUTEX_DEFAULT等同俺抽。
PTHREAD_MUTEX_ERRORCHECK 查錯(cuò)鎖:以損失些許性能的方式返回錯(cuò)誤信息敞映;
PTHREAD_MUTEX_RECURSIVE就是遞歸鎖;

可以通過(guò)pthread_mutexattr_t屬性設(shè)置鎖的類型磷斧,示例代碼如下:

- (void)pthread_mutex_recursiveDemo {
    
    // init attr
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
//    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);

    // init mutex
    __block pthread_mutex_t mutex_recursive;
    pthread_mutex_init(&mutex_recursive, &attr);
    pthread_mutexattr_destroy(&attr);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
        static void (^RecursiveMethod)(int);
        RecursiveMethod = ^(int value) {
            // lock
            pthread_mutex_lock(&mutex_recursive);
            if (value > 0) {
                NSLog(@"value = %d,thread = %@",value,[NSThread currentThread]);
                RecursiveMethod(value - 1);
            }else{
                pthread_mutex_destroy(&mutex_recursive);
            }
            // unlock
            pthread_mutex_unlock(&mutex_recursive);
        };
        
        RecursiveMethod(5);
    });
    //    使用完畢后振愿,銷毀
    //    pthread_mutex_destroy(& mutex_recursive);
}

// 打印結(jié)果:
2021-02-27 21:18:33.418542+0800 lockDemo[83366:6460107] value = 5,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}
2021-02-27 21:18:33.418716+0800 lockDemo[83366:6460107] value = 4,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}
2021-02-27 21:18:33.418845+0800 lockDemo[83366:6460107] value = 3,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}
2021-02-27 21:18:33.419116+0800 lockDemo[83366:6460107] value = 2,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}
2021-02-27 21:18:33.419250+0800 lockDemo[83366:6460107] value = 1,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}

注意: pthread_mutex(recursive)只保證在單線程情況下可重入,當(dāng)多個(gè)線程獲取相同的pthread_mutex(recursive)鎖會(huì)導(dǎo)致死鎖的發(fā)生弛饭。

pthread_rwlock(讀寫鎖)

基本上所有的問(wèn)題都可以用互斥的方案去解決冕末,但是可以解決并不代表適合。

pthread_mutex(互斥鎖)有個(gè)缺點(diǎn)孩哑,就是只要鎖住了栓霜,不管其他線程要干什么,都不允許進(jìn)入臨界區(qū)横蜒。設(shè)想這樣一種情況:臨界區(qū)變量a正在被線程1讀取胳蛮,加了個(gè)mutex鎖销凑,線程2如果也要讀變量a,因?yàn)楸痪€程1加了個(gè)互斥鎖仅炊,就只能等待線程1讀取完畢斗幼。但事實(shí)情況是,讀取數(shù)據(jù)并不影響數(shù)據(jù)內(nèi)容本身抚垄,所以即便被1個(gè)線程讀著蜕窿,另外一個(gè)線程也應(yīng)該被允許去讀。除非另外一個(gè)線程是寫操作呆馁,為了避免數(shù)據(jù)不一致的問(wèn)題桐经,寫線程就需要等讀線程都結(jié)束了再寫。

因此誕生了讀寫鎖浙滤,有的地方也叫共享-獨(dú)占鎖阴挣。

讀寫鎖的特性是這樣的,當(dāng)一個(gè)線程加了讀鎖訪問(wèn)臨界區(qū)纺腊,另外一個(gè)線程也想訪問(wèn)臨界區(qū)讀取數(shù)據(jù)的時(shí)候畔咧,也可以加一個(gè)讀鎖,這樣另外一個(gè)線程就能夠成功進(jìn)入臨界區(qū)進(jìn)行讀操作了揖膜。此時(shí)讀鎖線程有兩個(gè)誓沸。當(dāng)?shù)谌齻€(gè)線程需要進(jìn)行寫操作時(shí),它需要加一個(gè)寫鎖壹粟,這個(gè)寫鎖只有在讀鎖的擁有者為0時(shí)才有效拜隧。也就是等前兩個(gè)讀線程都釋放讀鎖之后,第三個(gè)線程就能進(jìn)去寫了趁仙『缧睿總結(jié)一下就是:

  • 當(dāng)讀寫鎖被一個(gè)線程以讀模式占用的時(shí)候,寫操作的其他線程會(huì)被阻塞幸撕,讀操作的其他線程還可以繼續(xù)進(jìn)行。
  • 當(dāng)讀寫鎖被一個(gè)線程以寫模式占用的時(shí)候外臂,寫操作的其他線程會(huì)被阻塞坐儿,讀操作的其他線程也被阻塞。

這樣更精細(xì)的控制宋光,就能減少mutex導(dǎo)致的阻塞延遲時(shí)間貌矿。如果受保護(hù)的數(shù)據(jù)結(jié)構(gòu)經(jīng)常被讀取,并且只偶爾修改罪佳,則可以顯著提高性能逛漫。雖然用mutex也能起作用,但這種場(chǎng)合赘艳,明顯讀寫鎖更好酌毡。

pthread中讀寫鎖主要函數(shù)如下:

// 靜態(tài)初始化方法
PTHREAD_RWLOCK_INITIALIZER
// 動(dòng)態(tài)初始化克握,可傳pthread_rwlockattr_t屬性
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
// 銷毀 pthread_rwlock
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 獲得讀鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 嘗試獲得讀鎖
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
// 獲得寫鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 嘗試獲得寫鎖
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 釋放鎖
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

使用讀寫鎖與pthread_mutex類似,都是通過(guò)初始化創(chuàng)建鎖枷踏,之后根據(jù)讀寫不同場(chǎng)景進(jìn)行加鎖菩暗、解鎖操作,在使用完畢后別忘了銷毀鎖旭蠕。示例代碼如下:

- (void)pthread_rwlock_demo {
    pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
    _rwlock = rwlock;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 讀
        [self readWithTag:1];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 讀
        [self readWithTag:2];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 寫
        [self writeWithTag:3];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 寫
        [self writeWithTag:4];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 讀
        [self readWithTag:5];
    });
    //使用完畢后銷毀鎖:不可在未使用完畢前銷毀
    //pthread_rwlock_destroy(&_rwlock);
}

- (void)readWithTag:(NSInteger )tag {
    pthread_rwlock_rdlock(&_rwlock);
    NSLog(@"start read ---- %ld",tag);
    self.path = [[NSBundle mainBundle] pathForResource:@"pthread_rwlock" ofType:@".txt"];
    self.content = [NSString stringWithContentsOfFile:self.path encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"end   read ---- %ld",tag);
    pthread_rwlock_unlock(&_rwlock);
}

- (void) writeWithTag:(NSInteger)tag {
    pthread_rwlock_wrlock(&_rwlock);
    NSLog(@"start wirte ---- %ld",tag);
    [self.content writeToFile:self.path atomically:YES encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"end   wirte ---- %ld",tag);
    pthread_rwlock_unlock(&_rwlock);
}

// 打印結(jié)果 :  讀操作可共享停团,寫操作互斥
2021-02-27 21:29:44.081500+0800 lockDemo[82462:10201536] start read ---- 2
2021-02-27 21:29:44.081500+0800 lockDemo[82462:10201541] start read ---- 1
2021-02-27 21:29:44.081795+0800 lockDemo[82462:10201536] end   read ---- 2
2021-02-27 21:29:44.081795+0800 lockDemo[82462:10201541] end   read ---- 1
2021-02-27 21:29:44.082017+0800 lockDemo[82462:10201537] start wirte ---- 3
2021-02-27 21:29:44.082182+0800 lockDemo[82462:10201537] end   wirte ---- 3
2021-02-27 21:29:44.082351+0800 lockDemo[82462:10201535] start wirte ---- 4
2021-02-27 21:29:44.082459+0800 lockDemo[82462:10201535] end   wirte ---- 4
2021-02-27 21:29:44.082617+0800 lockDemo[82462:10201538] start read ---- 5
2021-02-27 21:29:44.082799+0800 lockDemo[82462:10201538] end   read ---- 5

注意事項(xiàng)

避免寫線程饑餓
由于讀寫鎖的性質(zhì),在默認(rèn)情況下是很容易出現(xiàn)寫線程饑餓的掏熬。因?yàn)樗仨氁鹊剿凶x鎖都釋放之后佑稠,才能成功申請(qǐng)寫鎖。比如在寫線程阻塞的時(shí)候旗芬,有很多讀線程是可以一個(gè)接一個(gè)地在那兒插隊(duì)的(在默認(rèn)情況下舌胶,只要有讀鎖在,寫鎖就無(wú)法申請(qǐng)岗屏,然而讀鎖可以一直申請(qǐng)成功辆琅,就導(dǎo)致所謂的插隊(duì)現(xiàn)象),那么寫線程就不知道什么時(shí)候才能申請(qǐng)成功寫鎖了这刷,然后它就餓死了婉烟。所以要注意鎖建立后的優(yōu)先級(jí)問(wèn)題。不過(guò)不同系統(tǒng)的實(shí)現(xiàn)版本對(duì)寫線程的優(yōu)先級(jí)實(shí)現(xiàn)不同暇屋。Solaris下面就是寫線程優(yōu)先似袁,其他系統(tǒng)默認(rèn)讀線程優(yōu)先。

pthread_cond (條件變量)

當(dāng)我們?cè)谑褂枚嗑€程的時(shí)候咐刨,有時(shí)一把只會(huì)lock和unlock的鎖未必就能完全滿足我們的使用昙衅。因?yàn)槠胀ǖ逆i只能關(guān)心鎖與不鎖,而不在乎用什么鑰匙(滿足什么條件)才能開(kāi)鎖定鸟,而我們?cè)谔幚碣Y源共享的時(shí)候而涉,有時(shí)候需要只有滿足一定條件的情況下才能打開(kāi)這把鎖。

這時(shí)候联予,POSIX提供的pthread_cond(條件變量)就派上了用場(chǎng)啼县。主要的函數(shù)如下:

// 靜態(tài)初始化
PTHREAD_COND_INITIALIZER
// 動(dòng)態(tài)初始化并允許設(shè)置屬性
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
// 銷毀條件變量
int pthread_cond_destroy(pthread_cond_t *cond);
// 發(fā)送信號(hào)(給指定線程)
int pthread_cond_signal(pthread_cond_t *cond);
// 廣播信號(hào)(給所有線程)
int pthread_cond_broadcast(pthread_cond_t *cond);
// 等待信號(hào)
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 等待信號(hào),如果在指定時(shí)間仍未收到信號(hào)沸久,則返回
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mute

條件變量可以做到讓一個(gè)線程等待多個(gè)線程的結(jié)束季眷,并在合適的時(shí)候喚醒正在等待的線程,具體是什么時(shí)候卷胯,取決于你設(shè)置的條件是否滿足子刮。

示例代碼如下:

pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean        ready_to_go = false;
 
void MyCondInitFunction()
{
    mutex =  (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
    pthread_cond_init(&condition, NULL);
}
 
void MyWaitOnConditionFunction()
{
    // Lock the mutex.
    pthread_mutex_lock(&mutex);
    // If the predicate is already set, then the while loop is bypassed;
    // otherwise, the thread sleeps until the predicate is set.
    while(ready_to_go == false)
    {
        pthread_cond_wait(&condition, &mutex);
    }
    
    // Do work. (The mutex should stay locked.)
   
    // Reset the predicate and release the mutex.
    ready_to_go = false;
    pthread_mutex_unlock(&mutex);
}

void SignalThreadUsingCondition()
{
    // At this point, there should be work for the other thread to do.
    pthread_mutex_lock(&mutex);
    
    ready_to_go = true;
    // Signal the other thread to begin work.
    pthread_cond_signal(&condition);
    pthread_mutex_unlock(&mutex);
}

- (void)pthread_cont_demo {
    MyCondInitFunction();
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        MyWaitOnConditionFunction();
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        SignalThreadUsingCondition();
    });
}

補(bǔ)充一下,原則上pthread_cond_signal是只通知一個(gè)線程挺峡,pthread_cond_broadcast是用于通知很多線程。但POSIX標(biāo)準(zhǔn)也允許讓pthread_cond_signal用于通知多個(gè)線程,不強(qiáng)制要求只允許通知一個(gè)線程绑莺。具體看各系統(tǒng)的實(shí)現(xiàn)纺裁。

另外,在調(diào)用pthread_cond_wait之前,必須要申請(qǐng)互斥鎖围肥,當(dāng)線程通過(guò)pthread_cond_wait進(jìn)入waiting狀態(tài)時(shí)穆刻,會(huì)釋放傳入的互斥鎖。

NSLock (互斥鎖)

NSLockCocoa 基于pthread_mutex實(shí)現(xiàn)的一個(gè)基本的互斥鎖猪勇。對(duì)應(yīng)pthread_mutex的PTHREAD_MUTEX_ERRORCHECK的類型助析。遵循NSLocking協(xié)議,該協(xié)議定義了lock和unlock方法。通過(guò)lockunlock來(lái)進(jìn)行鎖定和解鎖。

實(shí)際上女淑,OC層面的基于pthread_mutex封裝的鎖對(duì)象都遵循NSLocking協(xié)議笛厦,這樣設(shè)計(jì)的目的是因?yàn)椋瑢?duì)于這些鎖的鎖定與解鎖行為對(duì)于底層的操作是一致的俺夕。使用這些方法來(lái)獲取和釋放鎖裳凸,就像使用任何pthread_mutex一樣。

除了NSLocking協(xié)議提供的標(biāo)準(zhǔn)鎖定行為劝贸,NSLock類還添加了tryLocklockBeforeDate:方法姨谷。

  • tryLock方法嘗試獲取該鎖,但如果該鎖不可用映九,并不會(huì)阻塞梦湘,該方法只返回NO。

  • lockBeforeDate:方法嘗試獲取鎖,但是如果在指定Date的時(shí)間限制內(nèi)沒(méi)有獲得鎖捌议,則會(huì)解除線程阻塞(并返回NO)哼拔。

使用示例如下:

- (void)nslock_demo {
    //主線程
    NSLock *lock = [[NSLock alloc] init];
    //線程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [lock lock];
        NSLog(@"線程1任務(wù) 開(kāi)始");
        sleep(2);
        NSLog(@"線程1任務(wù) 結(jié)束");
        [lock unlock];
    });
    //線程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        if ([lock tryLock]) {//嘗試獲取鎖,如果獲取不到返回NO瓣颅,不會(huì)阻塞該線程
            NSLog(@"線程2嘗試獲取鎖倦逐,鎖可用");
            [lock unlock];
        }else{
            NSLog(@"線程2嘗試獲取鎖,鎖不可用");
        }
        
        NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];
        if ([lock lockBeforeDate:date]) {//嘗試在未來(lái)的3s內(nèi)獲取鎖宫补,并阻塞該線程檬姥,如果3s內(nèi)獲取不到恢復(fù)線程, 返回NO,不會(huì)阻塞該線程
            NSLog(@"沒(méi)有超時(shí),線程2獲得鎖");
            [lock unlock];
        }else{
            NSLog(@"超時(shí)守谓,線程2沒(méi)有獲得鎖");
        }
    });
}

// 打印結(jié)果:
2021-02-27 21:44:10.071157+0800 lockDemo[36464:983765] 線程1任務(wù) 開(kāi)始
2021-02-27 21:44:11.074331+0800 lockDemo[36464:983761] 線程2嘗試獲取鎖穿铆,鎖不可用
2021-02-27 21:44:12.074832+0800 lockDemo[36464:983765] 線程1任務(wù) 結(jié)束
2021-02-27 21:44:12.075065+0800 lockDemo[36464:983761] 沒(méi)有超時(shí),線程2獲得鎖

NSRecursiveLock (遞歸鎖)

NSRecursiveLock是Cocoa對(duì)pthread_mutex互斥鎖 PTHREAD_MUTEX_RECURSIVE類型的封裝斋荞。與pthread_mutex(遞歸鎖)一樣荞雏,主要是用在循環(huán)或遞歸操作中。該鎖可以被同一個(gè)線程多次獲取平酿,而不會(huì)被阻塞凤优。它記錄了成功獲得鎖的次數(shù),每一次成功的獲得鎖蜈彼,都必須有一個(gè)配套的釋放鎖與其對(duì)應(yīng)筑辨,只有當(dāng)所有的加鎖和解鎖調(diào)用都被平衡后,鎖才會(huì)被實(shí)際釋放幸逆,以便其他線程能夠獲取它棍辕。

除了實(shí)現(xiàn)NSLocking協(xié)議的方法外,NSRecursiveLock還提供了兩個(gè)方法还绘,分別如下:

// 在給定的時(shí)間之前去嘗試請(qǐng)求一個(gè)鎖
- (BOOL)lockBeforeDate:(NSDate *)limit

// 嘗試去請(qǐng)求一個(gè)鎖楚昭,并會(huì)立即返回一個(gè)布爾值,表示嘗試是否成功
- (BOOL)tryLock

使用示例如下:

- (void)NSRecursiveLock_demo {
    //主線程
    NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
    //線程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void(^MyRecursiveFunction)(int);
        MyRecursiveFunction = ^(int value)
        {
            [recursiveLock lock];
            if (value > 0)
            {
                NSLog(@"遞歸任務(wù)1--%d",value);
                sleep(2);
                --value;
                MyRecursiveFunction(value);
            }
            [recursiveLock unlock];
        };
        MyRecursiveFunction(5);
    });
    //線程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        [recursiveLock lock];
        NSLog(@"任務(wù)2");
        [recursiveLock unlock];
    });
}

// 打印結(jié)果如下:
2021-02-27 21:48:33.853605+0800 lockDemo[83298:10293307] 遞歸任務(wù)1--5
2021-02-27 21:48:35.856179+0800 lockDemo[83298:10293307] 遞歸任務(wù)1--4
2021-02-27 21:48:37.859868+0800 lockDemo[83298:10293307] 遞歸任務(wù)1--3
2021-02-27 21:48:39.863572+0800 lockDemo[83298:10293307] 遞歸任務(wù)1--2
2021-02-27 21:48:41.868646+0800 lockDemo[83298:10293307] 遞歸任務(wù)1--1
2021-02-27 21:48:43.870858+0800 lockDemo[83298:10293303] 任務(wù)2

注意:由于遞歸鎖只有在所有鎖操作與解鎖操作得到平衡后才會(huì)被釋放拍顷,長(zhǎng)時(shí)間持有任何鎖會(huì)導(dǎo)致其他線程阻塞抚太,直到遞歸完成。如果可以通過(guò)重寫代碼來(lái)消除遞歸昔案,或者消除使用遞歸鎖的需要尿贫,那么可能會(huì)獲得更好的性能。

NSCondition (條件)

NSCondition是對(duì)POSIX條件pthread_cond的封裝踏揣, 它將所需的鎖和條件數(shù)據(jù)結(jié)構(gòu)包裝在一個(gè)對(duì)象中庆亡。使得開(kāi)發(fā)者可以像鎖定互斥鎖一樣鎖定它,然后像等待條件一樣等待它捞稿。

NSConditionNSLock身冀、@synchronized等是不同的是钝尸,NSCondition可以給每個(gè)線程分別加鎖,加鎖后不影響其他線程進(jìn)入臨界區(qū)搂根。其它線程也能上鎖,而之后可以根據(jù)條件決定是否繼續(xù)運(yùn)行線程铃辖,即線程是否要進(jìn)入 waiting 狀態(tài).

除了實(shí)現(xiàn)NSLocking協(xié)議的方法外剩愧,NSCondition還提供了以下函數(shù):

- (void)wait;   // 等待信號(hào)
- (BOOL)waitUntilDate:(NSDate *)limit;  // 等待信號(hào),如果limit時(shí)間已到娇斩,則直接返回
- (void)signal; // 發(fā)送信號(hào)
- (void)broadcast; // 廣播信號(hào)

通過(guò)NSCondition可以實(shí)現(xiàn)不同線程的調(diào)度仁卷。一個(gè)線程被某一個(gè)條件所阻塞,直到另一個(gè)線程滿足該條件從而發(fā)送信號(hào)給該線程使得該線程可以正確的執(zhí)行犬第。

- (void)NSCondition_demo {
    __block NSInteger timeToDoWork = 0;
    NSCondition *cocoaCondition = [[NSCondition alloc] init];
    // 線程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [cocoaCondition lock];
        while (timeToDoWork <= 0){
            [cocoaCondition wait];
        }
         
        timeToDoWork--;
         
        // Do real work here.
         
        [cocoaCondition unlock];
    });
    
    // 線程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(2);
        [cocoaCondition lock];
        timeToDoWork++;
        [cocoaCondition signal];
        [cocoaCondition unlock];
    });
}

NSConditionLock (條件鎖)

NSConditionLock是對(duì)NSCondition的進(jìn)一步封裝锦积,條件鎖對(duì)象所定義的互斥鎖可以用特定的值(某個(gè)條件)鎖定和解鎖。除了NSLocking協(xié)議外歉嗓,NSConditionLock還提供如下函數(shù)與屬性:

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;  // 當(dāng)condition的值滿足條件時(shí) 獲取鎖
- (BOOL)tryLock; // 嘗試獲得鎖丰介,不管是否獲得成功都立即返回,不阻塞線程
- (BOOL)tryLockWhenCondition:(NSInteger)condition; // 當(dāng)condition的值滿足條件時(shí)鉴分,嘗試加鎖
- (void)unlockWithCondition:(NSInteger)condition; // 釋放鎖哮幢,并將condition的值修改為執(zhí)行值
- (BOOL)lockBeforeDate:(NSDate *)limit; // 在指定時(shí)間限制內(nèi)獲取鎖,獲取失敗志珍,返回NO
// 在指定時(shí)間內(nèi)橙垢,當(dāng)condition的值滿足條件時(shí)獲取鎖
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

通常,當(dāng)線程需要以特定的順序執(zhí)行任務(wù)時(shí)伦糯,比如當(dāng)一個(gè)線程生產(chǎn)數(shù)據(jù)另一個(gè)線程消耗數(shù)據(jù)時(shí)柜某,可以使用NSConditionLock對(duì)象。在生產(chǎn)者執(zhí)行時(shí)敛纲,可以通過(guò)特定的條件獲得鎖(條件本身只是定義的一個(gè)整數(shù)值)喂击,當(dāng)生產(chǎn)者完成時(shí),它將解鎖载慈,并將鎖的條件設(shè)置為可以喚醒消費(fèi)者線程的條件惭等。

下面的示例展示了如何使用條件鎖處理生產(chǎn)者-消費(fèi)者問(wèn)題。假設(shè)一個(gè)應(yīng)用程序包含一個(gè)數(shù)據(jù)隊(duì)列办铡。生產(chǎn)者線程向隊(duì)列中添加數(shù)據(jù)辞做,消費(fèi)者線程從隊(duì)列中提取數(shù)據(jù)。生成器不需要等待特定的條件寡具,但是它必須等待鎖可用秤茅,這樣它才能安全地將數(shù)據(jù)添加到隊(duì)列中。

NSMutableArray *products = [NSMutableArray array];
NSConditionLock *lock = [[NSConditionLock alloc] init];
NSInteger HAS_DATA = 1;
NSInteger NO_DATA = 0;
    
dispatch_async(dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_DEFAULT), ^{
       
    while (1) {
        [lock lockWhenCondition:NO_DATA];
        [products addObject:[[NSObject alloc] init]];
        NSLog(@"produce a product, 總量: %zi", products.count);
        [lock unlockWithCondition:HAS_DATA];
        sleep(1);
    }
});
    
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
    while (1) {
        NSLog(@"wait for product");
        [lock lockWhenCondition:HAS_DATA];
        [products removeObjectAtIndex:0];
        NSLog(@"custome a product");
        [lock unlockWithCondition:NO_DATA];
    }
});

當(dāng)生產(chǎn)者釋放鎖的時(shí)候童叠,把條件設(shè)置成了1框喳。這樣消費(fèi)者可以獲得該鎖课幕,進(jìn)而執(zhí)行程序,如果消費(fèi)者獲得鎖的條件和生產(chǎn)者釋放鎖時(shí)給定的條件不一致五垮,則消費(fèi)者永遠(yuǎn)無(wú)法獲得鎖乍惊,也不能執(zhí)行程序。同樣放仗,如果消費(fèi)者釋放鎖給定的條件和生產(chǎn)者獲得鎖給定的條件不一致的話润绎,則生產(chǎn)者也無(wú)法獲得鎖,程序也不能執(zhí)行诞挨。

注意

  1. unlock 與 unlockWithCondition:(NSInteger)condition 的區(qū)別:
    • unlock:釋放鎖但并不改變condition的值莉撇;
    • unlockWithCondition:釋放鎖,并將condition的值修改為指定值惶傻。
  2. 由于在實(shí)現(xiàn)操作系統(tǒng)時(shí)的細(xì)微參與棍郎,即使代碼里沒(méi)有實(shí)際發(fā)出信號(hào),條件鎖也允許以虛假的成功返回银室。為了避免由這些假信號(hào)引起的問(wèn)題涂佃,您應(yīng)該始終將謂詞與條件鎖結(jié)合使用。謂詞是確定線程繼續(xù)執(zhí)行是否安全的更具體的方法粮揉。這個(gè)條件只是讓線程處于休眠狀態(tài)巡李,直到發(fā)送信號(hào)的線程可以設(shè)置謂詞。

@sychronized

@sychronized是使用起來(lái)最簡(jiǎn)單的互斥鎖扶认,通常只需要@sychronized(obj)這樣一個(gè)簡(jiǎn)單的指令就可以實(shí)現(xiàn)加/解鎖操作侨拦。

- (void)sychronized_demo {
    NSObject *obj = [[NSObject alloc] init];
    NSObject *obj1 = [[NSObject alloc] init];
    //線程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized(obj){
            NSLog(@"任務(wù)1");
            sleep(5);
        }
    });
    //線程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized(obj){
            NSLog(@"任務(wù)2");
        }
    });
}
// 打印結(jié)果:
2021-02-27 22:08:25.288126+0800 lockDemo[83702:10333558] 任務(wù)1
2021-02-27 22:08:30.291985+0800 lockDemo[83702:10333557] 任務(wù)2

@synchronized指令使用傳入的對(duì)象(obj)作為該鎖的唯一標(biāo)識(shí),只有當(dāng)標(biāo)識(shí)相同時(shí)辐宾,才為滿足互斥狱从,如果線程2中的@synchronized(obj)改為@synchronized(obj1),線程2就不會(huì)被阻塞叠纹。

// 如果將線程2的 @synchronized(obj)換成 @synchronized(obj1)季研,則
2021-02-27 22:09:42.831004+0800 lockDemo[83783:10344549] 任務(wù)1
2021-02-27 22:09:42.831014+0800 lockDemo[83783:10344546] 任務(wù)2

同時(shí)@synchronized還允許重入,前面提到的pthread_mutex(遞歸鎖)和NSRecursiveLock也支持重入誉察,但它們只允許在同一線程內(nèi)多次重入与涡,而@synchronized支持多線程重入。這是因?yàn)?code>@sychronized內(nèi)部持偏,除了維護(hù)了同一線程的加鎖次數(shù)lockCount外驼卖,還維護(hù)了使用唯一標(biāo)識(shí)的線程數(shù)threadCount

@synchronized指令實(shí)現(xiàn)鎖的優(yōu)點(diǎn)就是我們不需要在代碼中顯式的創(chuàng)建鎖對(duì)象鸿秆,便可以實(shí)現(xiàn)鎖的機(jī)制酌畜,但作為一種預(yù)防措施,@synchronized塊會(huì)隱式的添加一個(gè)異常處理例程來(lái)保護(hù)代碼卿叽,該處理例程會(huì)在異常拋出的時(shí)候自動(dòng)的釋放互斥鎖桥胞。所以如果不想讓隱式的異常處理例程帶來(lái)額外的開(kāi)銷恳守,你可以考慮使用鎖對(duì)象。

注意:確保傳入@synchronized的obj不為nil贩虾,因?yàn)槿绻麄魅氲膐bj為nil的話催烘,實(shí)際上并不會(huì)做任何實(shí)際的內(nèi)容,也無(wú)法達(dá)到加鎖的目的缎罢。

dispatch_semaphore信號(hào)量

dispatch_semaphoreNSCondition類似颗圣,都是一種基于信號(hào)的同步方式,但NSCondition信號(hào)只能發(fā)送屁使,不能保存(如果沒(méi)有線程在等待,則發(fā)送的信號(hào)會(huì)失效)奔则。而 dispatch_semaphore能保存發(fā)送的信號(hào)蛮寂。dispatch_semaphore 的核心是 dispatch_semaphore_t 類型的信號(hào)量。

dispatch_semaphore是信號(hào)量易茬,但當(dāng)信號(hào)總量設(shè)為 1 時(shí)也可以當(dāng)作鎖來(lái)酬蹋。在沒(méi)有等待情況出現(xiàn)時(shí),它的性能比 pthread_mutex 還要高抽莱,但一旦有等待情況出現(xiàn)時(shí)范抓,性能就會(huì)下降許多。相對(duì)于 OSSpinLock 來(lái)說(shuō)食铐,它的優(yōu)勢(shì)在于等待時(shí)不會(huì)消耗 CPU 資源匕垫。

與其相關(guān)的主要有三個(gè)函數(shù):

  • dispatch_semaphore_t dispatch_semaphore_create(long value)
    輸出一個(gè)dispatch_semaphore_t類型且值為value的信號(hào)量。值得注意的是虐呻,這里的傳入的參數(shù)value必須大于或等于0象泵,否則dispatch_semaphore_create會(huì)返回NULL。

  • long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
    這個(gè)函數(shù)會(huì)使傳入的信號(hào)量dsema的值加1斟叼;

  • long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
    這個(gè)函數(shù)會(huì)使傳入的信號(hào)量dsema的值減1偶惠;
    這個(gè)函數(shù)的作用是這樣的,如果dsema信號(hào)量的值大于0朗涩,該函數(shù)所處線程就繼續(xù)執(zhí)行下面的語(yǔ)句忽孽,并且將信號(hào)量的值減1;如果desema的值為0谢床,那么這個(gè)函數(shù)就阻塞當(dāng)前線程等待timeout(注意timeout的類型為dispatch_time_t兄一,不能直接傳入整形或float型數(shù)),如果等待的期間desema的值被dispatch_semaphore_signal函數(shù)加1了萤悴,且該函數(shù)(即dispatch_semaphore_wait)所處線程獲得了信號(hào)量瘾腰,那么就繼續(xù)向下執(zhí)行并將信號(hào)量減1。如果等待期間沒(méi)有獲取到信號(hào)量或者信號(hào)量的值一直為0覆履,那么等到timeout時(shí)蹋盆,其所處線程自動(dòng)執(zhí)行其后語(yǔ)句费薄。

示例代碼如下:

dispatch_semaphore_t signal = dispatch_semaphore_create(1);
    dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_semaphore_wait(signal, overTime);
            NSLog(@"需要線程同步的操作1 開(kāi)始");
            sleep(2);
            NSLog(@"需要線程同步的操作1 結(jié)束");
        dispatch_semaphore_signal(signal);
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        dispatch_semaphore_wait(signal, overTime);
            NSLog(@"需要線程同步的操作2");
        dispatch_semaphore_signal(signal);
    });
    
//執(zhí)行結(jié)果為:
需要線程同步的操作1 開(kāi)始
需要線程同步的操作1 結(jié)束
需要線程同步的操作2

OSSpinLock 自旋鎖

OSSpinLock是一把自旋鎖,性能很高栖雾。因?yàn)樗恢笔莇o while忙等狀態(tài)楞抡。這種自旋鎖的缺點(diǎn)是當(dāng)?shù)却龝r(shí)會(huì)消耗大量CPU資源,所以它不適用于較長(zhǎng)時(shí)間的任務(wù)析藕。

OSSpinLock是整數(shù)類型召廷,約定是解鎖為零,鎖定為非零账胧。鎖必須自然對(duì)齊竞慢,并且不能在緩存抑制的內(nèi)存中。

如果鎖已經(jīng)被持有治泥,OSSpinLockLock()將自旋筹煮,但會(huì)使用各種各樣的策略來(lái)后退,使其對(duì)大多數(shù)優(yōu)先級(jí)反轉(zhuǎn)活鎖免疫居夹。但因?yàn)樗梢孕D(zhuǎn)败潦,所以在某些情況下可能效率低下。

如果鎖被持有准脂,OSSpinLockTry()立即返回false劫扒,如果它獲得了鎖,則返回true狸膏。它不自旋沟饥。 OSSpinLockUnlock()通過(guò)置零無(wú)條件地解鎖鎖。

OSSpinLock 示例

- (void)osspinlock_demo {
    __block OSSpinLock theLock = OS_SPINLOCK_INIT;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(&theLock);
        NSLog(@"線程1");
        sleep(5);
        OSSpinLockUnlock(&theLock);
        NSLog(@"線程1解鎖成功");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        OSSpinLockLock(&theLock);
        NSLog(@"線程2");
        OSSpinLockUnlock(&theLock);
    });
}
// 打印結(jié)果
2021-02-27 22:05:13.526 ThreadLockControlDemo[2856:316247] 線程1
2021-02-27 22:05:23.528 ThreadLockControlDemo[2856:316247] 線程1解鎖成功
2021-02-27 22:05:23.529 ThreadLockControlDemo[2856:316260] 線程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)題,從而破壞了OSSpinLock汹族。

具體來(lái)說(shuō)萧求,如果一個(gè)低優(yōu)先級(jí)的線程獲得鎖并訪問(wèn)共享資源,這時(shí)一個(gè)高優(yōu)先級(jí)的線程也嘗試獲得這個(gè)鎖顶瞒,它會(huì)處于OSSpinLock的忙等狀態(tài)從而占用大量 CPU夸政。此時(shí)低優(yōu)先級(jí)線程無(wú)法與高優(yōu)先級(jí)線程爭(zhēng)奪 CPU 時(shí)間,從而導(dǎo)致任務(wù)遲遲完不成榴徐、無(wú)法釋放OSSpinLock守问。

所以從iOS10.0開(kāi)始匀归,蘋果棄用了OSSpinLock,并用os_unfair_lock進(jìn)行替代耗帕。不過(guò)穆端,os_unfair_lock的實(shí)現(xiàn)屬于互斥鎖,當(dāng)鎖被占用的時(shí)候仿便,線程處于阻塞狀態(tài)体啰,而非忙等。

iOS中的鎖的性能

在iOS中嗽仪,各種鎖的性能如下圖所示:


16239819199365.jpg

從圖中可以看出荒勇,在iOS中的鎖性能從高往底依次是:

  • OSSpinLock(自旋鎖)
  • dispatch_semaphone(信號(hào)量)
  • pthread_mutex(互斥鎖)
  • NSLock(互斥鎖)
  • NSCondition(條件鎖)
  • pthread_mutex(recursive 互斥遞歸鎖)
  • NSRecursiveLock(遞歸鎖)
  • NSConditionLock(條件鎖)
  • synchronized(互斥鎖)

性能總結(jié):

  1. OSSpinLock自旋鎖由于安全性問(wèn)題,在iOS10之后已經(jīng)被廢棄闻坚,其底層的實(shí)現(xiàn)用os_unfair_lock替代
    • 使用OSSpinLock會(huì)處于忙等待狀態(tài)
    • 使用os_unfair_lock會(huì)處于休眠狀態(tài)
  2. atomic原子鎖自帶一把自旋鎖枕屉,只能保證setter、getter時(shí)的線程安全鲤氢,在日常開(kāi)發(fā)中使用更多的還是nonatomic修飾屬性
    • atomic:當(dāng)屬性在調(diào)用setter、getter方法時(shí)西潘,會(huì)加上自旋鎖OSSpinLock卷玉,用于保證同一時(shí)刻只能有一個(gè)線程調(diào)用屬性的讀或?qū)懀苊饬藢傩宰x寫不同步的問(wèn)題喷市。由于是底層編譯器自動(dòng)生成的互斥鎖代碼相种,會(huì)導(dǎo)致效率相對(duì)較低
    • nonatomic:當(dāng)屬性在調(diào)用setter、getter方法時(shí)品姓,不會(huì)加上自旋鎖寝并,即線程不安全。由于編譯器不會(huì)自動(dòng)生成互斥鎖代碼腹备,可以提高效率
  3. @synchronized在底層維護(hù)了一個(gè)哈希表進(jìn)行線程data的存儲(chǔ)衬潦,通過(guò)鏈表表示可重入(即嵌套)的特性,雖然性能較低植酥,但由于簡(jiǎn)單好用镀岛,使用頻率很高
  4. NSLock、NSRecursiveLock底層是對(duì)pthread_mutex的封裝
  5. NSConditionNSConditionLock是條件鎖友驮,底層都是對(duì)pthread_mutex的封裝漂羊,當(dāng)滿足某一個(gè)條件時(shí)才能進(jìn)行操作,和信號(hào)量dispatch_semaphore類似
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末卸留,一起剝皮案震驚了整個(gè)濱河市走越,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌耻瑟,老刑警劉巖旨指,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赏酥,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡淤毛,警方通過(guò)查閱死者的電腦和手機(jī)今缚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)低淡,“玉大人姓言,你說(shuō)我怎么就攤上這事≌崽#” “怎么了何荚?”我有些...
    開(kāi)封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)猪杭。 經(jīng)常有香客問(wèn)我餐塘,道長(zhǎng),這世上最難降的妖魔是什么皂吮? 我笑而不...
    開(kāi)封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任戒傻,我火速辦了婚禮,結(jié)果婚禮上蜂筹,老公的妹妹穿的比我還像新娘需纳。我一直安慰自己,他們只是感情好艺挪,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布不翩。 她就那樣靜靜地躺著,像睡著了一般麻裳。 火紅的嫁衣襯著肌膚如雪口蝠。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天津坑,我揣著相機(jī)與錄音妙蔗,去河邊找鬼。 笑死疆瑰,一個(gè)胖子當(dāng)著我的面吹牛灭必,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播乃摹,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼禁漓,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了孵睬?” 一聲冷哼從身側(cè)響起播歼,我...
    開(kāi)封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后秘狞,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體叭莫,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年烁试,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了雇初。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡减响,死狀恐怖靖诗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情支示,我是刑警寧澤刊橘,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站颂鸿,受9級(jí)特大地震影響促绵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嘴纺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一败晴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧栽渴,春花似錦位衩、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)僚祷。三九已至佛致,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間辙谜,已是汗流浹背俺榆。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留装哆,地道東北人罐脊。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蜕琴,于是被迫代替她去往敵國(guó)和親萍桌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容