如何保證iOS的多線程安全

什么會(huì)給多線程的安全造成隱患?

有了多線程技術(shù)支持,我們可以并發(fā)的進(jìn)行多個(gè)任務(wù)棚赔,因此同一塊資源就有可能在多個(gè)線程中同時(shí)被訪問(讀/寫)拼苍。這個(gè)現(xiàn)象叫作資源共享,比如多個(gè)線程同時(shí)訪問了同一個(gè)對(duì)象吐辙,同一個(gè)變量或同一個(gè)文件宣决,這樣就有可能引發(fā)數(shù)據(jù)錯(cuò)亂何數(shù)據(jù)安全問題。

經(jīng)典問題一——存錢取錢

存錢取錢問題
下面通過代碼展示一下該問題

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self moneyTest];
}

//存錢取錢測(cè)試
-(void)moneyTest {
    self.money = 100;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [self saveMoney];
        }
        
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self drawMoney];
        }
    });
}

-(void)saveMoney {
    NSInteger oldMoney = self.money;
    sleep(.2);//模擬任務(wù)時(shí)長昏苏,便于問題顯現(xiàn)
    oldMoney += 50;
    self.money = oldMoney;
    NSLog(@"存了50元尊沸,賬戶余額%ld-------%@",(long)oldMoney, [NSThread currentThread]);
}

-(void)drawMoney {
    NSInteger oldMoney = self.money;
    sleep(.2);//模擬任務(wù)時(shí)長,便于問題顯現(xiàn)
    oldMoney -= 20;
    self.money = oldMoney;
    NSLog(@"取了20元贤惯,賬戶余額%ld-------%@",(long)oldMoney, [NSThread currentThread]);
}

我們?cè)?code>moneyTest方法中洼专,以多線程方式分別進(jìn)行了10次的存錢取錢操作,每次存50救巷,每次取20壶熏,存款初值為100,目標(biāo)余額應(yīng)該為100+ (50*10) - (20*10) = 400浦译,運(yùn)行結(jié)果如下

多線程存錢取錢問題

可以看出最后的余額數(shù)不對(duì)棒假。


經(jīng)典問題二——賣票問題

賣票問題
下面通過代碼展示一下該問題

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self sellTicketTest];
}

//賣票問題
-(void)sellTicketTest {
    self.ticketsCount = 30;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<5; i++) {
            [self sellTicket];
        }
        
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<5; i++) {
            [self sellTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<5; i++) {
            [self sellTicket];
        }
    });
}

-(void)sellTicket {
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(.2);//模擬任務(wù)時(shí)長,便于問題顯現(xiàn)
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@"還剩%ld張票-------%@",(long)oldTicketsCount, [NSThread currentThread]);
}

sellTicketTest里面精盅,起始票數(shù)30帽哑,通過3條線程同時(shí)賣票,每條線程賣10張叹俏,最有應(yīng)該全部賣完才對(duì)妻枕,運(yùn)行程序,結(jié)果如下

多線程賣票問題

打印結(jié)果看出最后的剩余票數(shù)發(fā)生了錯(cuò)誤。

上述兩個(gè)經(jīng)典問題都是由于多條線程對(duì)同一資源進(jìn)行了讀寫操作而導(dǎo)致的屡谐。用一個(gè)大家都熟悉的圖片來表示就是

針對(duì)這個(gè)問題的解決方案:使用線程同步技術(shù)(同步述么,就是協(xié)同步調(diào),按預(yù)定的先后次序進(jìn)行)愕掏。常見的線程同步技術(shù)就是:加鎖度秘。
先用下圖概括一下

線程加鎖


線程同步(加鎖)方案

iOS中的線程同步方案有如下幾種:

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock
  • @synchronized

下面我們來依次體驗(yàn)一下。

(一)OSSpinLock
OSSpinLock叫作”自旋鎖“饵撑,需要導(dǎo)入頭文件#import <libkern/OSAtomic.h>剑梳。它有如下API

  • OSSpinLock lock = OS_UNFAIR_LOCK_INIT; ——初始化鎖對(duì)象lock
  • OSSpinLockTry(&lock);——嘗試加鎖,加鎖成功繼續(xù)滑潘,加鎖失敗返回垢乙,繼續(xù)執(zhí)行后面的代碼,不阻塞線程
  • OSSpinLockLock(&lock);——加鎖语卤,加鎖失敗會(huì)阻塞線程追逮,進(jìn)行等待
  • OSSpinLockUnlock(&lock);——解鎖

下面來看一下代碼中如何使用它。以下代碼承接上面的賣票案例進(jìn)行加鎖操作

@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketsCount;
@end

*******************************************************

@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self sellTicketTest];
//    [self moneyTest];
}
//賣票問題
-(void)sellTicketTest {
    self.ticketsCount = 30;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [self sellTicket];
        }
        
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
}

-(void)sellTicket {
    //初始化鎖
    OSSpinLock lock = OS_SPINLOCK_INIT;
    //加鎖????????
    OSSpinLockLock(&lock);
    
    
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(.2);//模擬任務(wù)時(shí)長粱侣,便于問題顯現(xiàn)
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@"還剩%ld張票-------%@",(long)oldTicketsCount, [NSThread currentThread]);
    
    //解鎖????????
    OSSpinLockUnlock(&lock);
}
@end

運(yùn)行后結(jié)果

結(jié)果看沒成功羊壹,怎么回事呢蓖宦?這里補(bǔ)充一下加鎖的原理:一張圖搞定
加鎖原理

所以根據(jù)上圖的原理齐婴,我們應(yīng)該使用同一個(gè)鎖對(duì)象來給某個(gè)操作(代碼段)加鎖。所以將上面的鎖寫成一個(gè)全局性質(zhì)的屬性即可稠茂,代碼如下

@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketsCount;
@property (nonatomic, assign) OSSpinLock lock;
@end
*******************************************************
@implementation ViewController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self sellTicketTest];
}

//賣票問題
-(void)sellTicketTest {
    self.ticketsCount = 30;
    //初始化鎖
    self.lock = OS_SPINLOCK_INIT;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [self sellTicket];
        }
        
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
}

-(void)sellTicket {
    
    //加鎖????????
    OSSpinLockLock(&_lock);
    
    
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(.2);//模擬任務(wù)時(shí)長柠偶,便于問題顯現(xiàn)
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@"還剩%ld張票-------%@",(long)oldTicketsCount, [NSThread currentThread]);
    
    //解鎖????????
    OSSpinLockUnlock(&_lock);
}
@end

這樣最后的結(jié)果就沒問題了
image.png

賣票的問題,我們針對(duì)的是同一個(gè)操作來處理的睬关,而存錢取錢的問題诱担,涉及到了兩個(gè)操作(存錢操作和取錢操作),來看看該怎么處理电爹。首先要明確問題蔫仙,加鎖機(jī)制是為了解決從多條線程同時(shí)訪問共享資源所所產(chǎn)生的數(shù)據(jù)問題,無論這些線程里面執(zhí)行的是相同的操作(比如賣票)丐箩,還是不同的操作(存錢取錢)摇邦,所以可以認(rèn)為跟操作是沒關(guān)系的,問題的本質(zhì)是需要確定清楚屎勘,哪些操作是不能同時(shí)進(jìn)行的施籍,然后對(duì)這些操作使用相同的鎖對(duì)象進(jìn)行加鎖。

因此概漱,由于存錢操作和取錢操作也是不能同時(shí)進(jìn)行的(就是說不能同時(shí)兩條線程存錢丑慎,不能同時(shí)兩條線程取錢,也不能同時(shí)兩條線程分別存錢和取錢),因此我們需要對(duì)存錢操作和取錢操作使用相同的鎖對(duì)象進(jìn)行加鎖竿裂。
代碼如下

@interface ViewController ()
@property (nonatomic, assign) NSInteger money;
@property (nonatomic, assign) OSSpinLock lock;
@end

****************************************

@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//    [self sellTicketTest];
    [self moneyTest];
}
//存錢取錢問題
-(void)moneyTest {
    self.money = 100;
    //初始化鎖
    self.lock = OS_SPINLOCK_INIT;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [self saveMoney];
        }
        
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self drawMoney];
        }
    });
}

-(void)saveMoney {
    
    //加鎖????????
    OSSpinLockLock(&_lock);
    
    NSInteger oldMoney = self.money;
    sleep(.2);//模擬任務(wù)時(shí)長玉吁,便于問題顯現(xiàn)
    oldMoney += 50;
    self.money = oldMoney;
    NSLog(@"存了50元,賬戶余額%ld-------%@",(long)oldMoney, [NSThread currentThread]);
    //解鎖????????
    OSSpinLockUnlock(&_lock);
    
}

-(void)drawMoney {
    //加鎖????????
    OSSpinLockLock(&_lock);
    
    NSInteger oldMoney = self.money;
    sleep(.2);//模擬任務(wù)時(shí)長腻异,便于問題顯現(xiàn)
    oldMoney -= 20;
    self.money = oldMoney;
    NSLog(@"取了20元诈茧,賬戶余額%ld-------%@",(long)oldMoney, [NSThread currentThread]);
    //解鎖????????
    OSSpinLockUnlock(&_lock);
}

@end

最后得到了正確的結(jié)果
自旋鎖原理

自旋鎖的原理是當(dāng)加鎖失敗的時(shí)候,讓線程處于忙等的狀態(tài)(busy-wait)捂掰,以此讓線程停留在臨界區(qū)(需要加鎖的代碼段)之外敢会,一旦加鎖成功,線程便可以進(jìn)入臨界區(qū)進(jìn)行對(duì)共享資源操作这嚣。

讓線程阻塞有兩種方法:

  • 一種是讓線程真正休眠鸥昏,RunLoop里面用到的mach_msg()實(shí)現(xiàn)的效果就屬于這一種,它借住系統(tǒng)內(nèi)核指令姐帚,真正使得線程停下來吏垮,CPU不再 分配資源給線程,因此不會(huì)再執(zhí)行任何一句匯編指令罐旗。我們后面要介紹的互斥鎖膳汪,也是屬于屬于這種,它的底層匯編調(diào)用了一個(gè)系統(tǒng)函數(shù)syscall使得線程進(jìn)入休眠
  • 另一種就是自旋鎖的忙等九秀,本質(zhì)上是一個(gè)while循環(huán)遗嗽,不斷地去判斷加鎖條件,一旦當(dāng)前已經(jīng)進(jìn)入臨界區(qū)(加鎖代碼塊)的線程完成了操作鼓蜒,解開鎖之后痹换,等待鎖的線程便可以成功加鎖,再次進(jìn)入臨界區(qū)都弹。自旋鎖其實(shí)并沒有真正讓線程停下來娇豫,線程只不過是暫時(shí)被困在while循環(huán)里面,CPU還是在不斷的分配資源去處理它的匯編指令的(while循環(huán)的匯編指令)畅厢。下面再通過賣票操作的匯編追蹤冯痢,驗(yàn)證一下自旋鎖的本質(zhì)到底是不是while循環(huán)。首先將代碼改造如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self sellTicketTest];
}

-(void)sellTicketTest {
    self.ticketsCount = 30;
    //初始化鎖
    self.lock = OS_SPINLOCK_INIT;
    //開啟10條線程進(jìn)行賣票操作
    for (int i = 0; i<10; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(sellTicket) object:nil] start];
    }
}


-(void)sellTicket {
    
    //加鎖????????
    OSSpinLockLock(&_lock);
    
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(600);//??????任務(wù)時(shí)長模擬為10分鐘框杜,方便匯編調(diào)試
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@"還剩%ld張票-------%@",(long)oldTicketsCount, [NSThread currentThread]);
    
    //解鎖????????
    OSSpinLockUnlock(&_lock);
}

我們?cè)谫u票方法的加鎖代碼處加一個(gè)斷點(diǎn)浦楣,運(yùn)行程序,第一條線程走到斷點(diǎn)處霸琴,可以加鎖成功

跳過斷點(diǎn)椒振,該線程繼續(xù)執(zhí)行下面的代碼,同時(shí)會(huì)有第二條來到加鎖代碼斷點(diǎn)處
然后從這里開始梧乘,我們進(jìn)入?yún)R編碼界面澎迎,開始追蹤底層的函數(shù)調(diào)用棧庐杨。我們過掉第一個(gè)斷點(diǎn),馬上就會(huì)有第二條線程來到斷點(diǎn)處夹供,此時(shí)該線程肯定會(huì)被阻塞灵份。Xcode里面在斷點(diǎn)調(diào)試界面下,可以通過Debug -> Debug Workflow -> Always Show Disassembly切換到匯編界面哮洽。我們可以在控制臺(tái)通過命令si執(zhí)行一句匯編代碼填渠,入當(dāng)前匯編指令是調(diào)用函數(shù)的話,便會(huì)進(jìn)入到該函數(shù)的匯編界面鸟辅。好下面開始調(diào)試
解讀一下上面匯編追蹤過程的函數(shù)調(diào)用棧:

  • sellTicket
  • OSSpinLockLock
  • _OSSpinLockLockSlow

_OSSpinLockLockSlow里面氛什,你會(huì)看到程序會(huì)在cmpl指令和jne指令之間循環(huán)滾動(dòng),如果你不了解arm64的匯編指令匪凉,那么這里可以先告訴你枪眉,cmpl是將兩個(gè)值進(jìn)行比較,jne是指如果不相等就跳轉(zhuǎn)(jump if not equal)再层,程序在這兩者之間不斷循環(huán)贸铜,相信只要有一定編程敏感度的話,應(yīng)該能猜出這個(gè)東西了吧聂受?對(duì)蒿秦,這就是一個(gè)while循環(huán),圖中框起來的幾句匯編碼蛋济,是一個(gè)典型的while循環(huán)匯編實(shí)現(xiàn)棍鳖。這樣,我們就證明了瘫俊,自旋鎖的本質(zhì)鹊杖,就是一個(gè)while循環(huán)。

自旋鎖為什么被拋棄

蘋果已經(jīng)建議開發(fā)者停止使用自旋鎖扛芽,因?yàn)樵诰€程優(yōu)先級(jí)的作用下,會(huì)產(chǎn)生【優(yōu)先級(jí)反轉(zhuǎn)】积瞒,使得自旋鎖卡住川尖,因此它不再安全了。

我們知道茫孔,計(jì)算機(jī)的CPU在同一時(shí)間叮喳,只能處理一條線程,對(duì)于單CPU來說缰贝,線程的并發(fā)馍悟,實(shí)際上是一種假象,是系統(tǒng)讓CPU以很小的時(shí)間間隔在線程之間來回切換剩晴,所以看上去多條線程好像是在同時(shí)進(jìn)行的锣咒。到了多核CUP時(shí)代侵状,確實(shí)是可以實(shí)現(xiàn)真正的線程并發(fā),但是CUP核心數(shù)畢竟是有限的毅整,而程序內(nèi)部的線程數(shù)量通橙ば郑肯定是遠(yuǎn)大于CPU的數(shù)量的,因此悼嫉,很多情況下我們面對(duì)的還是單CPU處理多線程的情況艇潭。基于這種場(chǎng)景戏蔑,需要了解一個(gè)概念叫作線程優(yōu)先級(jí)蹋凝,CPU會(huì)將盡可能多的時(shí)間(資源)分配給優(yōu)先級(jí)高的線程,知道了這一點(diǎn)总棵,下面通過圖來展示一下所謂的優(yōu)先級(jí)反轉(zhuǎn)問題

自旋鎖優(yōu)先級(jí)反轉(zhuǎn)問題

自旋鎖的while循環(huán)本質(zhì)仙粱,使得線程并沒有停下來,一般情況下彻舰,一條線程等待鎖時(shí)間不會(huì)太長伐割,選用自旋做來阻塞線程所消耗的CPU資源,要小于線程的休眠和喚醒所帶來的CPU資源開銷刃唤,因此自旋鎖是一種效率很高的加鎖機(jī)制隔心,但是優(yōu)先級(jí)反轉(zhuǎn)問題使得自旋鎖不再安全,鎖的最終目的是安全而不是效率尚胞,因此蘋果放棄了自旋鎖硬霍。

另外為什么RunLoop要選擇真正的線程休眠呢?因?yàn)閷?duì)于App來說笼裳,可能處于長時(shí)間的擱置狀態(tài)唯卖,而沒有任何用戶行為發(fā)生,不需要CPU管躬柬,對(duì)于這種場(chǎng)景拜轨,當(dāng)然是讓線程休眠更為節(jié)約性能。好了允青,自旋鎖的今生前世就介紹到這里橄碾,雖然它已成歷史,但是了解一下肯定是更好的颠锉。

(二)os_unfair_lock
蘋果建議開發(fā)這法牲,從iOS10.0之后,就應(yīng)該用os_unfair_lock來取代OSSpinLock琼掠,接下來我們就來了解一下這中鎖拒垃。

要使用os_unfair_lock,需要導(dǎo)入頭文件#import <os/lock.h>瓷蛙,它有如下API

  • os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
    初始化鎖對(duì)象lock
  • os_unfair_lock_trylock(&lock);
    嘗試加鎖悼瓮,加鎖成功繼續(xù)戈毒,加鎖失敗返回,繼續(xù)執(zhí)行后面的代碼谤牡,不阻塞線程
  • os_unfair_lock_lock(&lock);
    加鎖副硅,加鎖失敗會(huì)阻塞線程進(jìn)行等待
  • os_unfair_lock_unlock(&lock);
    解鎖

它的使用和OSSpinLock的方法一樣,此處不贅述翅萤,蘋果為了解決OSSpinLock的優(yōu)先級(jí)反轉(zhuǎn)問題恐疲,在os_unfair_lock中摒棄了忙等方式,用使線程真正休眠的方式套么,來阻塞線程培己,也就從根本上解決了之前的問題。

(三)pthread_mutex
pthread_mutex來自與pthread胚泌,是一個(gè)跨平臺(tái)的解決方案省咨。mutex意為互斥鎖,等待鎖的線程會(huì)處于休眠狀態(tài)玷室。與之相反的是我們之前介紹的第一種自旋鎖零蓉,它是不休眠的。它有如下API
初始化鎖的屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NOMAL);
初始化鎖
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
嘗試加鎖
pthread_mutex_trylock(&mutex);
加鎖
pthread_mutex_lock(&mutex);
解鎖
pthread_mutex_unlock(&mutex);
銷毀相關(guān)資源
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&attr);

先上一份完整的代碼案例穷缤,針對(duì)賣票問題

#import <pthread.h>

@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketsCount;
//pthread_mutex
@property (nonatomic, assign) pthread_mutex_t ticketMutexLock;
@end

@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self sellTicketTest];
}

//賣票問題
-(void)sellTicketTest {
    self.ticketsCount = 30;
  
//初始化屬性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    //初始化鎖pthread_mutex
    pthread_mutex_init(&_ticketMutexLock, &attr);
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [self sellTicket];
        }
        
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
}

-(void)sellTicket {
    
    //加鎖????????
    //加pthread_mutex
    pthread_mutex_lock(&_ticketMutexLock);
    
    
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(.2);//模擬任務(wù)時(shí)長敌蜂,便于問題顯現(xiàn)
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@"還剩%ld張票-------%@",(long)oldTicketsCount, [NSThread currentThread]);
    
    //解鎖????????
    //開鎖pthread_mutex
    pthread_mutex_unlock(&_ticketMutexLock);
}

-(void)dealloc {
    pthread_mutex_destroy(&_ticketMutexLock);
}
@end

可以看到除了初始化步驟比之前介紹的鎖稍微麻煩一點(diǎn),其他加鎖解鎖操作還是一樣的津肛。還有就是需要進(jìn)行鎖的釋放章喉。這里來介紹一下mutex的初始化方法

int pthread_mutex_init(pthread_mutex_t * __restrict,
        const pthread_mutexattr_t * _Nullable __restrict);

其中第一個(gè)參數(shù)就是需要進(jìn)行初始化的鎖對(duì)象,第二個(gè)參數(shù)是鎖對(duì)象的屬性身坐。為此秸脱,我們還需要專門生成屬性對(duì)象,通過 定義屬性對(duì)象 --> 初始化屬性對(duì)象 --> 設(shè)置屬性種類這三個(gè)步驟來完成部蛇,屬性的類別有以下幾類

/*
 * 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_DEFAULT = PTHREAD_MUTEX_NORMAL摊唇,它們都代表普通互斥鎖,
PTHREAD_MUTEX_ERRORCHECK表示檢查錯(cuò)誤鎖搪花,不常用遏片。
PTHREAD_MUTEX_RECURSIVE代表遞歸互斥鎖,這個(gè)一會(huì)介紹撮竿。

如果我們給鎖設(shè)定默認(rèn)屬性,那么可以用一句代碼pthread_mutex_init(mutex, NULL);來搞定的鎖的初始化笔呀,不用再配置屬性信息幢踏。其中參數(shù)NULL表示的就是初始化一個(gè)普通的互斥鎖

互斥鎖的底層實(shí)現(xiàn)
前面我們通過查看匯編,驗(yàn)證了自旋鎖的本質(zhì)實(shí)際上就是通過一個(gè)while循環(huán)達(dá)到阻塞線程的目的⌒硎Γ現(xiàn)在我們同樣通過匯編房蝉,來看看互斥鎖是怎么做的僚匆。
首先修改一下賣票方法如下

//賣票問題
-(void)sellTicketTest {
    self.ticketsCount = 30;

    //初始化屬性
    pthread_mutexattr_init(&_attr);
    pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_NORMAL);
    //初始化鎖
    pthread_mutex_init(&_ticketMutexLock, &_attr);
    //創(chuàng)建10條線程來執(zhí)行賣票操作
    for (int i = 0; i<10; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(sellTicket) object:nil] start];
    }
}

-(void)sellTicket {
    //加pthread_mutex
    pthread_mutex_lock(&_ticketMutexLock);
    
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(600);//模擬任務(wù)時(shí)長,便于問題顯現(xiàn)
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@"還剩%ld張票-------%@",(long)oldTicketsCount, [NSThread currentThread]);
    
    //開鎖pthread_mutex
    pthread_mutex_unlock(&_ticketMutexLock);
}

-(void)dealloc {
    pthread_mutex_destroy(&_ticketMutexLock);
    pthread_mutexattr_destroy(&_attr);
}

將上面代碼的賣票流程設(shè)置成600秒時(shí)長搭幻,方便我們定位問題咧擂。

首先過掉第一條線程的斷點(diǎn)

然后來到第二條線程的斷點(diǎn)

然后從該斷點(diǎn)處查看匯編
總結(jié)一下上面這幾個(gè)匯編跟蹤圖,互斥做的函數(shù)調(diào)用棧是這樣的:
  • sellTicket
  • pthread_mutex_lock
  • _pthread_mutex_firstfit_lock_slow
  • _pthread_mutex_firstfit_lock_wait
  • __psynch_mutexwait
  • syscall
    這個(gè)syscall代表系統(tǒng)調(diào)用檀蹋,一般是調(diào)用系統(tǒng)級(jí)別比較內(nèi)核的方法松申。當(dāng)我們從syscall在繼續(xù)執(zhí)行的話,整個(gè)斷點(diǎn)就消失了俯逾,因?yàn)樵摼€程此時(shí)開始休眠贸桶,不在執(zhí)行匯編代碼了,因此也就無法追蹤斷點(diǎn)了桌肴。

我在介紹os_unfair_lock時(shí)說過皇筛,它的本質(zhì)也是通過休眠來實(shí)現(xiàn)線程阻塞,因此可以把它歸類為互斥鎖坠七,但是網(wǎng)上也有文章分析說它是一種自旋鎖水醋,相信經(jīng)過對(duì)于自旋鎖和互斥鎖本質(zhì)的分析,以及通過匯編來驗(yàn)證的方法彪置,你應(yīng)該可以自己動(dòng)手實(shí)踐來驗(yàn)證一下拄踪,到底誰說的才對(duì)。這里不在重復(fù)上面的匯編過程悉稠,總之經(jīng)過匯編追蹤宫蛆,os_unfair_lock走到最后也是用了syscall,然后斷點(diǎn)消失的猛,跳出線程耀盗,通過此現(xiàn)象,相信你已經(jīng)能判斷os_unfair_lock的本質(zhì)了卦尊。換個(gè)角度理解叛拷,如果它真的是自旋鎖,那么【如何避免優(yōu)先級(jí)反轉(zhuǎn)問題】呢岂却?強(qiáng)烈建議你自己試試匯編調(diào)試忿薇,實(shí)踐出真知,蠻好玩的躏哩,也加深了記憶署浩。

互斥遞歸鎖
現(xiàn)在請(qǐng)看如下的場(chǎng)景

-(void)otherTest {
    NSLog(@"%s",__func__);
    [self otherTest2];    
}

-(void)otherTest2 { 
    NSLog(@"%s",__func__);
}

如果正常調(diào)用otherTest,結(jié)果大家肯定都知道扫尺,會(huì)是如下

2019-08-21 14:23:22.853271+0800 多線程安全[986:22420] -[ViewController otherTest]
2019-08-21 14:23:22.853388+0800 多線程安全[986:22420] -[ViewController otherTest2]

如果這兩段代碼都需要保證線程安全筋栋,我們通過加互斥鎖,來看一下效果

-(void)otherTest {
    
    
    //初始化屬性
    pthread_mutexattr_init(&_attr);
    pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_NORMAL);
    //初始化鎖
    pthread_mutex_init(&_mutex, &_attr);
    
    pthread_mutex_lock(&_mutex);
    NSLog(@"%s",__func__);
    [self otherTest2];
    pthread_mutex_unlock(&_mutex);
    
}

-(void)otherTest2 {
    pthread_mutex_lock(&_mutex);
    NSLog(@"%s",__func__);
    pthread_mutex_unlock(&_mutex);
}

******************打印結(jié)果**********************8
2019-08-21 14:33:15.816512+0800 多線程安全[1101:29283] -[ViewController otherTest]

如果給兩個(gè)方法都加上同一把鎖正驻,可以看到調(diào)用otherTest方法會(huì)導(dǎo)致線程卡該方法里面弊攘,只完成了打印代碼的執(zhí)行抢腐,就不能繼續(xù)往下走了。原因也很簡(jiǎn)單襟交,如下圖

死鎖的形成

要解決這個(gè)問題很簡(jiǎn)單迈倍,給兩個(gè)方法加上不同的鎖對(duì)象就可以解決了。

我們?cè)陂_發(fā)中如果碰到需要給遞歸函數(shù)加鎖捣域,如下面這個(gè)

-(void)otherTest {
    pthread_mutex_lock(&_mutex);
    
    NSLog(@"%s",__func__);
    //業(yè)務(wù)邏輯

    [self otherTest];
    
    pthread_mutex_unlock(&_mutex);
}

就無法通過不同的鎖對(duì)象來加鎖了啼染。只要是使用相同的鎖對(duì)象,有肯定會(huì)出現(xiàn)死鎖竟宋。針對(duì)這個(gè)問題pthread給我們提供了遞歸鎖來解決這個(gè)問題提完。要想使用遞歸鎖,只需要在初始化屬性的時(shí)候丘侠,選擇遞歸鎖屬性即可徒欣。其他的使用步驟跟普通互斥鎖沒有區(qū)別。

pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_RECURSIVE);

那么遞歸鎖是如何避免死鎖的呢蜗字?其實(shí)就是對(duì)于同一個(gè)鎖對(duì)象來說打肝,允許重復(fù)的加鎖,重復(fù)的解鎖挪捕,因?yàn)閷?duì)于一個(gè)有出口的遞歸函數(shù)來說粗梭,函數(shù)的調(diào)用次數(shù) = 函數(shù)的退出次數(shù),因此加鎖的次數(shù)pthread_mutex_lock和解鎖的次數(shù)pthread_mutex_unlock是相等的级零,所以遞歸函數(shù)結(jié)束時(shí)断医,所有的??都會(huì)被解開。

但是遞歸鎖只是針對(duì)在相同的線程里面可以重復(fù)加鎖和解鎖奏纪,這點(diǎn)要牢記鉴嗤。也就是除了單線程的遞歸函數(shù)調(diào)用,在其他場(chǎng)景下的重復(fù)加鎖 / 解鎖序调,遞歸鎖時(shí)起不了重復(fù)加鎖的作用的醉锅。



互斥鎖條件pthread_cond_t
首先先列舉一下相關(guān)API

  • pthread_mutex_t mutex;
    ——定義一個(gè)鎖對(duì)象
  • pthread_mutex_init(&mutex, NULL);
    ——初始化鎖對(duì)象
  • pthread_cond_t condition;
    ——定義一個(gè)條件對(duì)象
  • pthread_cond_init(&condition, NULL);
    ——初始化條件對(duì)象
  • pthread_cond_wait(&condition, &mutex);
    ——等待條件
  • pthread_cond_signal(&condition);
    ——激活一個(gè)等待該條件的線程
  • pthread_cond_broadcast(&condition);
    ——激活所有等待條件的線程
  • pthread_mutex_destroy(&mutex);
    ——銷毀鎖對(duì)象
  • pthread_cond_destroy(&condition);
    ——銷毀條件對(duì)象

為了解釋互斥鎖條件的作用,我們來設(shè)計(jì)一種場(chǎng)景案例:

  • 我們?cè)?code>remove方法里面對(duì)數(shù)組dataArr進(jìn)行刪除元素操作
  • add方法里面對(duì)dataArr進(jìn)行元素添加操作
  • 并且要求发绢,如果dataArr的元素個(gè)數(shù)為0硬耍,則不能進(jìn)行刪除操作

以下是案例代碼以及運(yùn)行結(jié)果

@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *dataArr;
//鎖對(duì)象
@property (nonatomic, assign) pthread_mutex_t mutex;
//條件對(duì)象
@property (nonatomic, assign) pthread_cond_t cond;
@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
        //初始化屬性
        pthread_mutexattr_t _attr;
        pthread_mutexattr_init(&_attr);
        pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_NORMAL);
        //初始化鎖
        pthread_mutex_init(&_mutex, &_attr);
        //屬性使用完,進(jìn)行釋放
        pthread_mutexattr_destroy(&_attr);
        //初始化條件
        pthread_cond_init(&_cond, NULL);
        //初始化數(shù)組
        self.dataArr = [NSMutableArray array];

    [self dataArrTest];
}

- (void)dataArrTest
{
    //先開啟線程進(jìn)行 remove边酒,此時(shí)dataArr為空
   NSThread *removeT =  [[NSThread alloc] initWithTarget:self selector:@selector(remove) object:nil];
    [removeT setName:@"REMOVE操作線程"];
    [removeT start];
    
    sleep(.1);
    
    //然后開啟線程進(jìn)行 add
    NSThread *addT = [[NSThread alloc] initWithTarget:self selector:@selector(add) object:nil];
    [addT setName:@"ADD操作線程"];
    [addT start];
}

//******往數(shù)組添加元素********
-(void)add {
    
    //加鎖
    pthread_mutex_lock(&_mutex);
    
    NSLog(@"[LOCK]%@加鎖成功-->add開始\n",[NSThread currentThread].name);
    sleep(2);
    [self.dataArr addObject:@"test"];
    NSLog(@"add成功经柴,dataArr內(nèi)有%lu個(gè)元素,發(fā)送條件信號(hào)-------->\n",(unsigned long)self.dataArr.count);
    
    //發(fā)送條件信號(hào)
    pthread_cond_signal(&_cond);
    
    //解鎖
    pthread_mutex_unlock(&_mutex);
    NSLog(@"[UNLOCK]%@解鎖成功墩朦,線程結(jié)束\n",[NSThread currentThread].name);
}


//********從字典刪除元素*******
-(void)remove {
    //加鎖
    pthread_mutex_lock(&_mutex);
    
    
    NSLog(@"[LOCK]%@加鎖成功-->remove開始\n",[NSThread currentThread].name);
    if (!self.dataArr.count) {
        //進(jìn)行條件等待
        NSLog(@"dataArr沒有元素口锭,開始等待~~~~~~~~\n");
        pthread_cond_wait(&_cond, &_mutex);
        NSLog(@"-------->接受到條件更新信號(hào),dataArr已經(jīng)有了元素介杆,繼續(xù)刪除操作\n");
    }
    [self.dataArr removeLastObject];
    NSLog(@"remove成功鹃操,dataArr內(nèi)還剩%lu個(gè)元素\n",(unsigned long)self.dataArr.count);
    
    //解鎖
    pthread_mutex_unlock(&_mutex);
    NSLog(@"[UNLOCK]%@解鎖成功,線程結(jié)束\n",[NSThread currentThread].name);
}


@end

運(yùn)行結(jié)果如下

2019-08-21 21:14:07.767881+0800 多線程安全[3751:204180] [LOCK]REMOVE操作線程加鎖成功-->remove開始

2019-08-21 21:14:07.768075+0800 多線程安全[3751:204180] dataArr沒有元素春哨,開始等待~~~~~~~~

2019-08-21 21:14:07.768216+0800 多線程安全[3751:204181] [LOCK]ADD操作線程加鎖成功-->add開始

2019-08-21 21:14:09.771757+0800 多線程安全[3751:204181] add成功沧踏,dataArr內(nèi)有1個(gè)元素产禾,發(fā)送條件信號(hào)-------->

2019-08-21 21:14:09.772048+0800 多線程安全[3751:204181] [UNLOCK]ADD操作線程解鎖成功,線程結(jié)束

2019-08-21 21:14:09.772081+0800 多線程安全[3751:204180] -------->接受到條件更新信號(hào),dataArr已經(jīng)有了元素末贾,繼續(xù)刪除操作

2019-08-21 21:14:09.772300+0800 多線程安全[3751:204180] remove成功,dataArr內(nèi)還剩0個(gè)元素

2019-08-21 21:14:09.772496+0800 多線程安全[3751:204180] [UNLOCK]REMOVE操作線程解鎖成功灵疮,線程結(jié)束
//

從案例以及運(yùn)行結(jié)果分析叽讳,互斥鎖的條件pthread_cond_t可以在線程加鎖之后,如果條件不達(dá)標(biāo)便瑟,暫停線程缆毁,等到條件符合標(biāo)準(zhǔn),繼續(xù)執(zhí)行線程到涂。這么描述還是比較抽象脊框,請(qǐng)看下圖

pthread_cond_t作用原理

總結(jié)一下pthread_cond_t的作用:

  • 首先在A線程內(nèi)碰到業(yè)務(wù)邏輯無法往下執(zhí)行的時(shí)候,調(diào)用pthread_cond_wait(&_cond, &_mutex);践啄,這句代碼首先會(huì)解鎖當(dāng)前線程浇雹,然后休眠當(dāng)前線程以等待條件信號(hào),
  • 此時(shí)屿讽,鎖已經(jīng)解開昭灵,那么之前等待鎖的B線程可以成功加鎖,執(zhí)行它后面的邏輯伐谈,由于B線程內(nèi)的某些操作完成后可以觸發(fā)A的運(yùn)行條件烂完,此時(shí)從B線程通過pthread_cond_signal(&_cond);向外發(fā)出條件信號(hào)。
  • A線程的收到了條件信號(hào)就會(huì)被pthread_cond_t喚醒衩婚,一旦B線程解鎖之后窜护,pthread_cond_t會(huì)在A線程內(nèi)重新加鎖,繼續(xù)A線程的后續(xù)操作非春,并最終解鎖柱徙。從前到后,有三次加鎖奇昙,三次解鎖护侮。
  • 通過pthread_cond_t就實(shí)現(xiàn)了一種線程與線程之間的依賴關(guān)系,實(shí)際開發(fā)中我們會(huì)有不少場(chǎng)景需要用到這種跨線程依賴關(guān)系储耐。

(四)NSLock羊初、NSRecursiveLock、NSCondition
上面我們了解了mutex普通鎖mutex遞歸鎖长赞、mutex條件鎖晦攒,都是基于C語言的API,蘋果在此基礎(chǔ)上得哆,進(jìn)行了一層面向?qū)ο蠓庋b脯颜,為開發(fā)者供了對(duì)應(yīng)的OC鎖如下

  • NSLock--->封裝了pthread_mutex_t(attr = 普通)
  • NSRecursiveLock--->封裝了pthread_mutex_t(attr = 遞歸)
  • NSCondition---> 封裝了pthread_mutex_t + pthread_cond_t

由于底層就是pthread_mutex,因此這里不再通過代碼案例演示贩据,因?yàn)槌藢懛ㄉ细鼮榉奖阒舛安伲矶际且粯拥模旅媪信e一下相關(guān)的API使用方法

//普通鎖
NSLock *lock = [[NSLock alloc] init];
[lock lock];
[lock unlock];
//遞歸鎖
NSRecursiveLock *rec_lock = [[NSRecursiveLock alloc] 
[rec_lock lock];
[rec_lock unlock];init];
//條件鎖
NSCondition *condition = [[NSCondition alloc] init];
[self.condition lock];
[self.condition wait];
[self.condition signal];
[self.condition unlock];

(五)NSConditionLock
蘋果總是希望開發(fā)者不要知道的太多饱亮,變得更懶矾芙,更加依賴他們的生態(tài),為此近上,基于NSCondition有進(jìn)一步封裝了NSConditionLock剔宪。該鎖允許我們?cè)阪i中設(shè)定條件具體條件值,有了這個(gè)功能戈锻,我們可以更加方便的多條線程的依賴關(guān)系和前后執(zhí)行順序歼跟。首先看一下相關(guān)API:
與之前鎖相同的一些功能

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

特色功能

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

接下來通過案例來說明它的功能

- (instancetype)init
{
    if (self = [super init]) {
        self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self otherTest];
}

- (void)otherTest
{
    [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start];
}

- (void)__one
{
    [self.conditionLock lock];
    
    NSLog(@"__one");
    sleep(1);
    
    [self.conditionLock unlockWithCondition:2];
}

- (void)__two
{
    [self.conditionLock lockWhenCondition:2];
    
    NSLog(@"__two");
    sleep(1);
    
    [self.conditionLock unlockWithCondition:3];
}

- (void)__three
{
    [self.conditionLock lockWhenCondition:3];
    
    NSLog(@"__three");
    
    [self.conditionLock unlock];
}

代碼實(shí)現(xiàn)的效果就是__one方法先執(zhí)行,再執(zhí)行__two方法格遭,最后執(zhí)行__three方法哈街。因?yàn)槿齻€(gè)方法是在三個(gè)不同的子線程里面,所以這里精確控制了三條線程的先后執(zhí)行順序拒迅,或者說依賴關(guān)系骚秦。再用下圖說明一下

NSConditionLock精確控制線程順序/依賴關(guān)系

上面介紹的這幾個(gè)NS開頭的鎖,都是屬于蘋果Foundation框架的璧微,沒有開源作箍,但是我們可以參考GNU_Foundation來大致了解這幾個(gè)NS鎖的內(nèi)部實(shí)現(xiàn)。關(guān)于GNU是什么前硫,請(qǐng)自己科普胞得。

(六)dispatch_queue(DISPATCH_QUEUE_SERIAL)

GCD的串行隊(duì)列也可以實(shí)現(xiàn)多線程同步,而且它并不是通過加鎖來實(shí)現(xiàn)的屹电。線程同步本質(zhì)上就是需要多個(gè)線程按照順序阶剑,線性的,一個(gè)接一個(gè)的去執(zhí)行危号,而GCD的串行隊(duì)列正好就是用來做這個(gè)的牧愁。下面直接通過代碼案例來演示一下

@interface GCDSerialQueueVC ()
@property (nonatomic, assign) NSInteger ticketsCount;
@property (nonatomic, assign) NSInteger money;
//賣票串行隊(duì)列,賣票操作都放在這個(gè)隊(duì)列
@property (strong, nonatomic) dispatch_queue_t ticketQueue;
//存取錢串行隊(duì)列外莲,存錢取錢操作都放在這個(gè)隊(duì)列
@property (strong, nonatomic) dispatch_queue_t moneyQueue;
@end

@implementation GCDSerialQueueVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.ticketQueue = dispatch_queue_create("ticketQueue", DISPATCH_QUEUE_SERIAL);
    self.moneyQueue = dispatch_queue_create("moneyQueue", DISPATCH_QUEUE_SERIAL);
    [self moneyTest];
    [self sellTicketTest];
}


//存錢取錢問題
-(void)moneyTest {
    self.money = 100;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [self saveMoney];
        }
        
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self drawMoney];
        }
    });
}

-(void)saveMoney {
    
    dispatch_sync(_moneyQueue, ^{//存錢操作放入存取錢隊(duì)列
        NSInteger oldMoney = self.money;
        sleep(.2);//模擬任務(wù)時(shí)長猪半,便于問題顯現(xiàn)
        oldMoney += 50;
        self.money = oldMoney;
        NSLog(@"存了50元,賬戶余額%ld-------%@",(long)oldMoney, [NSThread currentThread]);
    });
}

-(void)drawMoney {
    
    dispatch_sync(_moneyQueue, ^{//取錢操作放入存取錢隊(duì)列
        NSInteger oldMoney = self.money;
        sleep(.2);//模擬任務(wù)時(shí)長,便于問題顯現(xiàn)
        oldMoney -= 20;
        self.money = oldMoney;
        NSLog(@"取了20元磨确,賬戶余額%ld-------%@",(long)oldMoney, [NSThread currentThread]);
    });
}


//賣票問題
-(void)sellTicketTest {
    self.ticketsCount = 30;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [self sellTicket];
        }
        
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
}

-(void)sellTicket {
    dispatch_sync(_ticketQueue, ^{//賣票操作放入賣票隊(duì)列
        NSInteger oldTicketsCount = self.ticketsCount;
        sleep(.2);//模擬任務(wù)時(shí)長沽甥,便于問題顯現(xiàn)
        oldTicketsCount--;
        self.ticketsCount = oldTicketsCount;
        NSLog(@"還剩%ld張票-------%@",(long)oldTicketsCount, [NSThread currentThread]);
    });
}

@end

(七)dispatch_semaphore

除了上面的方案,GCD還為開發(fā)者提供了dispatch_semaphore方案來處理多線程同步問題俐填。semaphore意為“信號(hào)量”安接。信號(hào)量的初始值可以用來控制線程并發(fā)發(fā)訪問的最大數(shù)量。信號(hào)量的初始值為1英融,代表同時(shí)允許1條線程訪問資源,這樣就可以達(dá)到線程同步的目的歇式。下面來熟悉一下它的API:

  • dispatch_semaphore_create(value)根據(jù)一個(gè)初始值創(chuàng)建信號(hào)量
  • dispatch_semaphore_wait(semaphore, 等待時(shí)間)
    如果信號(hào)量的值<=0驶悟,當(dāng)前線程就會(huì)進(jìn)入休眠等待(直到信號(hào)量的值>0)
    如果信號(hào)量的值>0,就減1材失,然后往下執(zhí)行后面的代碼痕鳍。
  • dispatch_semaphore_signal(semaphore)讓信號(hào)量的值加1

如果我們將信號(hào)量初值設(shè)為1,那么多個(gè)線程運(yùn)行示意圖如下
GCD-semaphore控制線程同步

可以看到龙巨,線程會(huì)一個(gè)一個(gè)先后執(zhí)行笼呆,也就是說,同一時(shí)間旨别,只有一條線程可以執(zhí)行業(yè)務(wù)代碼诗赌,這就達(dá)到了線程同步的要求。據(jù)上推理秸弛,如果信號(hào)量初值設(shè)為2铭若,同一時(shí)間,就可以有兩條線程運(yùn)行递览,相當(dāng)于控制了線程并發(fā)執(zhí)行的數(shù)量叼屠。那么最后,在展示一下代碼案例

@interface GCDSemaphoreVC ()
@property (nonatomic, assign) NSInteger ticketsCount;
@property (nonatomic, assign) NSInteger money;

@property (strong, nonatomic) dispatch_semaphore_t semaphore;//并發(fā)測(cè)試信號(hào)量
@property (strong, nonatomic) dispatch_semaphore_t ticketSemaphore;//賣票測(cè)試信號(hào)量
@property (strong, nonatomic) dispatch_semaphore_t moneySemaphore;//存取錢測(cè)試信號(hào)量
@end

@implementation GCDSemaphoreVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.semaphore = dispatch_semaphore_create(5);
    self.ticketSemaphore = dispatch_semaphore_create(1);
    self.moneySemaphore = dispatch_semaphore_create(1);
    
    
    [self moneyTest];
    [self sellTicketTest];
    [self concurrencytest];
}


//最大并發(fā)數(shù)測(cè)試
- (void)concurrencytest
{
    for (int i = 0; i < 20; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(concurrencyOperationUnit) object:nil] start];
    }
}

- (void)concurrencyOperationUnit
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    sleep(2);
    NSLog(@"test - %@", [NSThread currentThread]);
    
    // 讓信號(hào)量的值+1
    dispatch_semaphore_signal(self.semaphore);
}


//存錢取錢問題
-(void)moneyTest {
    self.money = 100;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {//異步并發(fā)執(zhí)行10次存錢操作
            [self saveMoney];
        }
        
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {//異步并發(fā)執(zhí)行10次取錢操作
            [self drawMoney];
        }
    });
}

-(void)saveMoney {
    //????????????信號(hào)量-1
    dispatch_semaphore_wait(_ticketSemaphore, DISPATCH_TIME_FOREVER);
    //????????????存錢業(yè)務(wù)代碼************************
    NSInteger oldMoney = self.money;
    sleep(.2);//模擬任務(wù)時(shí)長绞铃,便于問題顯現(xiàn)
    oldMoney += 50;
    self.money = oldMoney;
    NSLog(@"存了50元镜雨,賬戶余額%ld-------%@",(long)oldMoney, [NSThread currentThread]);
    
    //????????????信號(hào)量+1
    dispatch_semaphore_signal(_ticketSemaphore);
}

-(void)drawMoney {
    //????????????信號(hào)量-1
    dispatch_semaphore_wait(_ticketSemaphore, DISPATCH_TIME_FOREVER);
    
    //????????????取錢業(yè)務(wù)代碼************************
    NSInteger oldMoney = self.money;
    sleep(.2);//模擬任務(wù)時(shí)長,便于問題顯現(xiàn)
    oldMoney -= 20;
    self.money = oldMoney;
    NSLog(@"取了20元儿捧,賬戶余額%ld-------%@",(long)oldMoney, [NSThread currentThread]);
    
    //????????????信號(hào)量-1
    dispatch_semaphore_signal(_ticketSemaphore);
}


//賣票問題
-(void)sellTicketTest {
    self.ticketsCount = 30;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i= 0; i<10; i++) {
            [self sellTicket];
        }
        
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i<10; i++) {
            [self sellTicket];
        }
    });
}

-(void)sellTicket {
    //????????????信號(hào)量-1
    dispatch_semaphore_wait(_ticketSemaphore, DISPATCH_TIME_FOREVER);
    
    //????????????賣票業(yè)務(wù)代碼************************
    NSInteger oldTicketsCount = self.ticketsCount;
    sleep(.2);//模擬任務(wù)時(shí)長荚坞,便于問題顯現(xiàn)
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@"還剩%ld張票-------%@",(long)oldTicketsCount, [NSThread currentThread]);
    
    //????????????信號(hào)量+1
    dispatch_semaphore_signal(_ticketSemaphore);
    
}

@end

(八)@synchronized
最后,我們?cè)賮斫榻B一種非常簡(jiǎn)單的線程同步方案——@synchronized纯命。相信大家或多或少都使用過或者看到過這個(gè)指令西剥。它的使用超級(jí)簡(jiǎn)單

@synchronized (lockObj) {
        /*
         加鎖代碼(臨界區(qū))
         */
    }

雖然使用簡(jiǎn)單,但是它是所有線程同步方案里面性能最差的亿汞。蘋果非常不建議我們使用瞭空,除非你是測(cè)試環(huán)境下,否則需要很謹(jǐn)慎地使用。對(duì)于移動(dòng)設(shè)備來說咆畏,什么最寶貴南捂,一個(gè)是存儲(chǔ)空間(內(nèi)存),一個(gè)就是CPU資源旧找。我們下面就通過底層來看一下@synchronized效率低下的原因溺健。

首先,通過匯編追蹤一下@synchronized的底層函數(shù)調(diào)用棧钮蛛,按圖中方法加上斷點(diǎn)

然后顯示匯編碼(Debug` -> `Debug Workflow` -> `Always Show Disassembly
從匯編看鞭缭,@synchronized實(shí)際上就是轉(zhuǎn)化成了objc_sync_enterobjc_cync_exit兩個(gè)函數(shù),包括在臨界區(qū)的頭和尾部魏颓。這兩個(gè)函數(shù)的源碼可以在在objc4中的objc-sync.mm文件中找到岭辣。


int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

通過上圖的分析,可以看出甸饱,@synchronized內(nèi)部最終使用的是pthread_mutex_t沦童,并且是遞歸的。@synchronized拿到參數(shù)obj之后叹话,會(huì)利用函數(shù)id2data(obj, ACQUIRE);得到SyncData* data偷遗,然后通過data->mutex拿到最終的鎖,最終進(jìn)行pthread_mutex_t的加鎖解鎖操作驼壶。

下面來看一下id2data(obj, ACQUIRE);是怎么拿到SyncData* data的氏豌。

我們看到,其實(shí)id2data(obj, ACQUIRE);是將obj作為key辅柴,從哈希表/字典sDataLists里面取出對(duì)應(yīng)的SyncData列表箩溃,最后經(jīng)過處理,再找到目標(biāo)SyncData* data碌嘀。
@synchronized的核心流程如下圖

正式由于@synchronized內(nèi)部封裝了數(shù)組涣旨,字典(哈希表)、C++的數(shù)據(jù)結(jié)構(gòu)等一系列復(fù)雜數(shù)據(jù)結(jié)構(gòu)股冗,導(dǎo)致它的實(shí)際性能特別底下霹陡,實(shí)際上是性能最低的線程同步,雖然你可能在一些牛逼框架里面看到過它被使用止状,但是如果你不是對(duì)底層特別熟練的話烹棉,還是按照蘋果的建議,少用為妙怯疤,因?yàn)樗娴暮芾速M(fèi)性能浆洗。

同步方案性能對(duì)比

以上,我們就iOS中的各路線程同步方案體驗(yàn)了一遍集峦。從原理上來說伏社,OSSpinLock由于其不休眠特性抠刺,所以它的效率是非常高的,但是由于安全問題摘昌,蘋果建議我們使用os_unfair_lock取而代之速妖,并且效率還要高于前者。pthread_mutex是一種跨平臺(tái)的解決方案聪黎,性能也不錯(cuò)罕容。當(dāng)然還有蘋果的GCD解決方案,也是挺不錯(cuò)的稿饰。對(duì)于NS開頭的那些OC下的解決方案锦秒,雖然本質(zhì)也還是基于pthread_mutex的封裝,但是由于多了一些面向?qū)ο蟮牟僮鏖_銷湘纵,效率不免要下降脂崔。性能最差的是@synchronized方案,雖然它的使用是最簡(jiǎn)單的梧喷,但因?yàn)樗牡讓臃庋b了過于復(fù)雜的數(shù)據(jù)結(jié)構(gòu),導(dǎo)致了性能底下脖咐。經(jīng)過各路大咖的實(shí)測(cè)和總結(jié)铺敌,將各種線程同步方案效率從高到低排列如下:

  • os_unfair_lock(推薦??????????)
  • OSSpinLock(不安全????)
  • dispatch_semaphore(推薦??????????)
  • pthread_mutex(推薦????????)
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)(推薦??????)
  • NSLock(??????)
  • NSCondition(??????)
  • pthread_mutex(recursive)(????)
  • NSRecursiveLock(????)
  • NSConditionLock(????)
  • @synchronized(最不推薦)

自旋鎖和互斥鎖的對(duì)比

什么情況下選擇自旋鎖更好?
自旋鎖特點(diǎn):效率高屁擅、安全性不足偿凭、占用CPU資源大,因此選擇自旋鎖依據(jù)原則如下:

  • 預(yù)計(jì)線程等待鎖的時(shí)間很短
  • 加鎖的代碼(臨界區(qū))經(jīng)常被調(diào)用派歌,但是競(jìng)爭(zhēng)的情況發(fā)生概率很小弯囊,對(duì)安全性要求不高
  • CPU資源不緊張
  • 多核處理器

什么情況下使用互斥鎖更好?
互斥鎖特點(diǎn):安全性突出胶果、占用CPU資源小匾嘱,休眠/喚醒過程要消耗CPU資源,因此選擇互斥鎖依據(jù)原則如下:

  • 預(yù)計(jì)線程等待鎖的時(shí)間比較長
  • 單核處理器
  • 臨界區(qū)有IO操作
  • 臨界區(qū)代碼復(fù)雜或者循環(huán)量大
  • 臨界區(qū)的競(jìng)爭(zhēng)非常激烈早抠,對(duì)安全性要求高

為什么iOS中幾乎不用atomic

atomic是用于保證setter霎烙、setter方法的原子性操作的,本質(zhì)就是在getter和setter內(nèi)部增加線程同步的鎖蕊连。我們可以在objc源碼的objc-accessors.mm中驗(yàn)證這一點(diǎn)悬垃,如下圖

getter方法

setter方法


可以看到,如果屬性被atomic修飾之后甘苍,getter方法和setter內(nèi)部實(shí)際上就是增加了線程同步鎖尝蠕,而且可以看到,用的鎖實(shí)質(zhì)上是os_unfair_lock载庭。這里需要理解的一個(gè)關(guān)鍵點(diǎn)是看彼,atomic只能保證getter方法和setter方法內(nèi)部的線程同步廊佩,例如對(duì)于屬性@property (atomic, strong) NSMutableArray *dataArr;,所謂共享資源闲昭,指的是這個(gè)屬性多對(duì)應(yīng)的_dataArr成員變量罐寨。getter方法和setter方法實(shí)際上只是兩段訪問了_dataArr的代碼段而已,atomic也僅僅能保證這兩段代碼被執(zhí)行時(shí)候的線程同步序矩,但是_dataArr完全有可能在其他地方被直接被訪問(不通過屬性訪問)鸯绿,這是atomic所覆蓋不到的區(qū)域,例如

NSMutableArray *arr = self.dataArr;//getter方法是安全的
for (int i = 0; i<5; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [arr addObject:@"1"];//這里會(huì)有多線程操作_dataArr,atomic無法保證這里的線程同步
    });
}

所以說atomic并不能完全保證多線程安全問題簸淀。

另外由于其實(shí)在實(shí)際操作中瓶蝴,我們不太會(huì)多個(gè)線程同時(shí)操縱同一個(gè)屬性,因此對(duì)于屬性的資源搶占問題其實(shí)并不突出租幕,另外property在iOS代碼中實(shí)在是在是調(diào)用太頻繁了舷手,使用atomic就會(huì)導(dǎo)致鎖的過度使用,太消耗CPU資源了劲绪,恰恰移動(dòng)設(shè)備上稀缺的就是這個(gè)男窟,所以使用atomic沒有太多實(shí)際意義,我們完全可以針對(duì)具體會(huì)出現(xiàn)多線程隱患的地方直接加鎖贾富,也就是說歉眷,需要加鎖的時(shí)候,再去加颤枪,這樣可以更有效的使用CPU汗捡。因此,iOS里面畏纲,我們幾乎不用atomic扇住,atomic主要用在mac開發(fā)當(dāng)中。

多線程讀寫安全

我們?cè)谟懻摯驽X取錢的問題是盗胀,其實(shí)存錢操作和取錢操作里面都包含了對(duì)共享資源的讀和寫艘蹋。假設(shè)我們有如下兩個(gè)操作分別只包含讀操作和寫操作

- (void)read {
    sleep(1);
    NSLog(@"read");
}

- (void)write
{
    sleep(1);
    NSLog(@"write");
}

其實(shí)讀操作的目的,只是取出數(shù)據(jù)读整,并不會(huì)修改數(shù)據(jù)簿训,比如我取出來只是為了打印一下,因此多線程同時(shí)進(jìn)行讀操作是沒問題的米间,不需要考慮線程同步問題强品。寫操作是導(dǎo)致多線程安全問題的根本因素。我們iOS中對(duì)文件的操作屈糊,就屬于典型的讀寫操作的榛,會(huì)有寫入文件和讀取文件。對(duì)于讀寫安全逻锐,解決方案其實(shí)就是多讀單寫夫晌,需要滿足一下幾條:

  • 要求1:同一時(shí)間雕薪,只能有1個(gè)線程進(jìn)行寫的操作
  • 要求2:同一時(shí)間,允許有多個(gè)線程進(jìn)行讀的操作
  • 要求3:同一時(shí)間晓淀,不允許既讀又寫所袁,就是說讀操作和寫操作之間是互斥關(guān)系

首先我們回顧一下iOS多線程同步的各種方案,可以發(fā)現(xiàn)凶掰,我們可以通過對(duì)寫操作加鎖燥爷,實(shí)現(xiàn)上面的【要求1】,不對(duì)讀操作加鎖懦窘,就可以實(shí)現(xiàn)【要求2】前翎,但是沒有一種方案可以在滿足【要求2】的前提下實(shí)現(xiàn)【要求3】,體會(huì)一下畅涂。

iOS中有兩種方案可以實(shí)現(xiàn)上述的讀寫安全需求

  • pthread_rwlock:讀寫鎖
  • dispatch_barrier_async:異步柵欄調(diào)用

pthread_rwlock
使用pthread_rwlock港华,等待鎖的線程會(huì)進(jìn)入休眠,需要用到的API如下

//初始化鎖
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);
//讀操作加鎖
pthread_rwlock_rdlock(&lock);
//讀操作嘗試加鎖
pthread_rwlock_tryrdlock(&lock);
//寫操作加鎖
pthread_rwlock_wrlock(&lock);
//寫操作嘗試加鎖
pthread_rwlock_trywrlock(&lock);
//解鎖
pthread_rwlock_unlock(&lock);
//銷毀鎖
pthread_rwlock_destroy(&lock);

由于使用比較簡(jiǎn)單午衰,這里就不上代碼案例了立宜,了解它的效果就可以了。

dispatch_barrier_async
使用dispatch_barrier_async有一個(gè)注意點(diǎn)臊岸,這個(gè)函數(shù)接受的并發(fā)隊(duì)列參數(shù)必須是你自己手動(dòng)創(chuàng)建的(dispatch_queue_create)赘理,如果接受的是一個(gè)串行隊(duì)列或者是一個(gè)全局并發(fā)隊(duì)列,那么這個(gè)函數(shù)的效果等同于dispatch_async函數(shù)扇单。具體的使用原則非常簡(jiǎn)單

//手動(dòng)創(chuàng)建一個(gè)并發(fā)隊(duì)列
    dispatch_queue_t queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        /*
         讀操作代碼
         */
    });
    dispatch_barrier_async(queue, ^{
        /*
         寫操作代碼
         */
    })

下面給出一個(gè)代碼案例

for (int i = 0; i < 10; i++) {
        dispatch_async(self.queue, ^{
            [self read];
        });
        
        dispatch_async(self.queue, ^{
            [self read];
        });
        
        dispatch_async(self.queue, ^{
            [self read];
        });
        
        dispatch_barrier_async(self.queue, ^{
            [self write];
        });
    }

總結(jié)一下就是

到此有關(guān)iOS中多線程的同步問題和多線程讀寫安全問題的解決方案,就整理到這里奠旺。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蜘澜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子响疚,更是在濱河造成了極大的恐慌鄙信,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忿晕,死亡現(xiàn)場(chǎng)離奇詭異装诡,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)践盼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門鸦采,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人咕幻,你說我怎么就攤上這事渔伯。” “怎么了肄程?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵锣吼,是天一觀的道長选浑。 經(jīng)常有香客問我,道長玄叠,這世上最難降的妖魔是什么古徒? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮读恃,結(jié)果婚禮上隧膘,老公的妹妹穿的比我還像新娘。我一直安慰自己狐粱,他們只是感情好舀寓,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著肌蜻,像睡著了一般互墓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蒋搜,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天篡撵,我揣著相機(jī)與錄音,去河邊找鬼豆挽。 笑死育谬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的帮哈。 我是一名探鬼主播膛檀,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼娘侍!你這毒婦竟也來了咖刃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤憾筏,失蹤者是張志新(化名)和其女友劉穎嚎杨,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體氧腰,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡枫浙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了古拴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片箩帚。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖斤富,靈堂內(nèi)的尸體忽然破棺而出膏潮,到底是詐尸還是另有隱情,我是刑警寧澤满力,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布焕参,位于F島的核電站轻纪,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏叠纷。R本人自食惡果不足惜刻帚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望涩嚣。 院中可真熱鬧崇众,春花似錦、人聲如沸航厚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽幔睬。三九已至眯漩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間麻顶,已是汗流浹背赦抖。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留辅肾,地道東北人队萤。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像矫钓,于是被迫代替她去往敵國和親要尔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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

  • Q:為什么出現(xiàn)多線程新娜? A:為了實(shí)現(xiàn)同時(shí)干多件事的需求(并發(fā))盈电,同時(shí)進(jìn)行著下載和頁面UI刷新。對(duì)于處理器杯活,為每個(gè)線...
    幸福相依閱讀 1,578評(píng)論 0 2
  • 一、簡(jiǎn)介:多線程在之前進(jìn)行過一篇詳細(xì)的基礎(chǔ)博客 iOS多線程 二熬词、多線程的基礎(chǔ)知識(shí)回顧 1.1旁钧、iOS中的常見多線...
    IIronMan閱讀 892評(píng)論 0 4
  • 目錄 1、為什么要線程安全 2互拾、自旋鎖和互斥鎖 3歪今、鎖的類型1、OSSpinLock2颜矿、os_unfair_loc...
    SunshineBrother閱讀 1,163評(píng)論 0 20
  • 線程安全是怎么產(chǎn)生的 常見比如線程內(nèi)操作了一個(gè)線程外的非線程安全變量寄猩,這個(gè)時(shí)候一定要考慮線程安全和同步。 - (v...
    幽城88閱讀 661評(píng)論 0 0
  • 目錄:1.為什么要線程安全2.多線程安全隱患分析3.多線程安全隱患的解決方案4.鎖的分類-13種鎖4.1.1OSS...
    二斤寂寞閱讀 1,183評(píng)論 0 3