[iOS] 多線(xiàn)程安全

1. 什么情況下會(huì)有線(xiàn)程隱患访递?

我們?cè)谑褂枚嗑€(xiàn)程技術(shù)帶來(lái)的便利的同時(shí),也需要考慮下多線(xiàn)程所帶來(lái)的隱患唯袄。比如,我們可以并發(fā)的進(jìn)行多個(gè)任務(wù)蜗帜,因此同一塊資源就可能在多個(gè)線(xiàn)程中同時(shí)被訪(fǎng)問(wèn)(讀/寫(xiě))恋拷,這個(gè)現(xiàn)象叫做資源共享,比如多個(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn)了同一個(gè)對(duì)象\變量\文件厅缺,這樣就有可能引發(fā)數(shù)據(jù)錯(cuò)亂數(shù)據(jù)安全問(wèn)題蔬顾。

2. 兩個(gè)常見(jiàn)的示例

2.1 示例:存錢(qián)取錢(qián)
存錢(qián)取錢(qián)問(wèn)題

我們用代碼來(lái)實(shí)現(xiàn)一下這個(gè)功能:

- (void)moneyTest{
    self.money = 1000;

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 10; i++) {
            [self saveMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 10; i++) {
            [self drawMoney];
        }
    });
}

// 存錢(qián)
- (void)saveMoney{
    NSInteger oldMoney = self.money;
    sleep(0.5);
    oldMoney += 50;
    self.money = oldMoney;
    NSLog(@"存了50元,賬戶(hù)余額 %ld湘捎,當(dāng)前線(xiàn)程 %@",self.money,[NSThread currentThread]);
}

// 取錢(qián)
- (void)drawMoney{
    NSInteger oldMoney = self.money;
    sleep(0.5);
    oldMoney -= 20;
    self.money = oldMoney;
    NSLog(@"取了20元诀豁,賬戶(hù)余額 %ld,當(dāng)前線(xiàn)程 %@",self.money,[NSThread currentThread]);
}

輸出結(jié)果:

2019-10-12 16:40:18.033330+0800 tianran[16615:4423457] 存了50元窥妇,賬戶(hù)余額 1050且叁,當(dāng)前線(xiàn)程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033486+0800 tianran[16615:4423457] 存了50元,賬戶(hù)余額 1080秩伞,當(dāng)前線(xiàn)程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033500+0800 tianran[16615:4423456] 取了20元逞带,賬戶(hù)余額 1030欺矫,當(dāng)前線(xiàn)程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.033573+0800 tianran[16615:4423457] 存了50元,賬戶(hù)余額 1130展氓,當(dāng)前線(xiàn)程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033632+0800 tianran[16615:4423456] 取了20元穆趴,賬戶(hù)余額 1110,當(dāng)前線(xiàn)程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.033659+0800 tianran[16615:4423457] 存了50元遇汞,賬戶(hù)余額 1160未妹,當(dāng)前線(xiàn)程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033730+0800 tianran[16615:4423456] 取了20元,賬戶(hù)余額 1140空入,當(dāng)前線(xiàn)程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.033737+0800 tianran[16615:4423457] 存了50元络它,賬戶(hù)余額 1190,當(dāng)前線(xiàn)程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033821+0800 tianran[16615:4423457] 存了50元歪赢,賬戶(hù)余額 1240化戳,當(dāng)前線(xiàn)程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033839+0800 tianran[16615:4423456] 取了20元,賬戶(hù)余額 1170埋凯,當(dāng)前線(xiàn)程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.033949+0800 tianran[16615:4423457] 存了50元点楼,賬戶(hù)余額 1220,當(dāng)前線(xiàn)程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.034040+0800 tianran[16615:4423456] 取了20元白对,賬戶(hù)余額 1200掠廓,當(dāng)前線(xiàn)程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.034178+0800 tianran[16615:4423457] 存了50元,賬戶(hù)余額 1250甩恼,當(dāng)前線(xiàn)程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.034341+0800 tianran[16615:4423456] 取了20元蟀瞧,賬戶(hù)余額 1230,當(dāng)前線(xiàn)程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.034423+0800 tianran[16615:4423457] 存了50元条摸,賬戶(hù)余額 1280悦污,當(dāng)前線(xiàn)程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.034581+0800 tianran[16615:4423456] 取了20元,賬戶(hù)余額 1260屈溉,當(dāng)前線(xiàn)程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.034665+0800 tianran[16615:4423457] 存了50元,賬戶(hù)余額 1310抬探,當(dāng)前線(xiàn)程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.034803+0800 tianran[16615:4423456] 取了20元子巾,賬戶(hù)余額 1290,當(dāng)前線(xiàn)程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.035029+0800 tianran[16615:4423456] 取了20元小压,賬戶(hù)余額 1270线梗,當(dāng)前線(xiàn)程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.035105+0800 tianran[16615:4423456] 取了20元,賬戶(hù)余額 1250怠益,當(dāng)前線(xiàn)程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}

我們?cè)?code>moneyTest方法中仪搔,以多線(xiàn)程方式分別進(jìn)行了10次的存/取錢(qián)操作,每次存50蜻牢,每次取20烤咧,最后執(zhí)行完之后余額應(yīng)該為1000 + (50 * 10) - (20 * 10) = 1300偏陪,但是上面輸出的結(jié)果是1250,很明顯煮嫌,出大問(wèn)題了笛谦。

2.2 示例:賣(mài)票問(wèn)題
賣(mài)票問(wèn)題

我們通過(guò)代碼展示一下這個(gè)問(wèn)題:

- (void)ticketTest{
    self.ticketNum = 30;

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

// 賣(mài)票操作
- (void)saleTicket{
    self.ticketNum -= 1;
    NSLog(@"還剩%ld張票,currentThread - %@",self.ticketNum,[NSThread currentThread]);
}

輸出結(jié)果:

2019-10-12 16:46:38.501449+0800 tianran[16620:4425096] 還剩29張票昌阿,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.501573+0800 tianran[16620:4425094] 還剩28張票饥脑,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.501593+0800 tianran[16620:4425096] 還剩27張票,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.501684+0800 tianran[16620:4425096] 還剩25張票懦冰,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.501693+0800 tianran[16620:4425094] 還剩24張票灶轰,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.501689+0800 tianran[16620:4425093] 還剩26張票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}
2019-10-12 16:46:38.501753+0800 tianran[16620:4425096] 還剩23張票刷钢,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.501776+0800 tianran[16620:4425094] 還剩22張票笋颤,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.501781+0800 tianran[16620:4425093] 還剩21張票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}
2019-10-12 16:46:38.501877+0800 tianran[16620:4425094] 還剩19張票闯捎,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.501819+0800 tianran[16620:4425096] 還剩20張票椰弊,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.502044+0800 tianran[16620:4425093] 還剩18張票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}
2019-10-12 16:46:38.502404+0800 tianran[16620:4425094] 還剩17張票瓤鼻,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.502529+0800 tianran[16620:4425093] 還剩16張票秉版,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}
2019-10-12 16:46:38.502760+0800 tianran[16620:4425093] 還剩15張票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}

雖然最后的結(jié)果是一樣的茬祷,但是我們可以看到中間賣(mài)票的時(shí)候清焕,票數(shù)有問(wèn)題了。

上面兩個(gè)問(wèn)題都是由于多個(gè)線(xiàn)程對(duì)同一資源進(jìn)行了讀寫(xiě)操作而導(dǎo)致的祭犯,下面用一個(gè)熟悉的圖片來(lái)表示下:

image.png

針對(duì)這個(gè)問(wèn)題的解決方案:使用線(xiàn)程同步技術(shù)(同步秸妥,就是協(xié)同步調(diào),按預(yù)定的先后次序進(jìn)行)沃粗。常見(jiàn)的線(xiàn)程同步技術(shù)就是:加鎖粥惧。

如下圖所示:


image.png

在進(jìn)行操作的時(shí)候,先加鎖最盅,保證此時(shí)只有一個(gè)線(xiàn)程對(duì)資源進(jìn)行操作突雪,操作完成后在解鎖。

3. 線(xiàn)程同步(加鎖)方案

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

借鑒一張鎖的性能數(shù)據(jù)對(duì)比圖涡贱,如下所示:

image.jpeg

介紹鎖之前咏删,我們可以先看幾個(gè)概念定義:

  • 臨界區(qū):
    指的是一塊對(duì)公共資源進(jìn)行訪(fǎng)問(wèn)的代碼,并非一種機(jī)制或算法问词。

  • 自旋鎖:
    用于多線(xiàn)程同步的一種鎖督函,線(xiàn)程反復(fù)檢查鎖變量是否可用。由于線(xiàn)程在這一過(guò)程中保持執(zhí)行,因此是一種忙等待辰狡。一旦獲取了自旋鎖锋叨,線(xiàn)程會(huì)一直保持該鎖,直至顯式的釋放自旋鎖搓译。自旋鎖避免了線(xiàn)程上下文的調(diào)度開(kāi)銷(xiāo)悲柱,因此對(duì)于線(xiàn)程只會(huì)阻塞很短時(shí)間的場(chǎng)景是非常有效的,如上面的 OSSpinLockatomic些己。

  • 互斥鎖(Mutex):
    是一種用于多線(xiàn)程編程中豌鸡,防止多個(gè)線(xiàn)程同時(shí)對(duì)同一公共資源(比如全局變量)進(jìn)行讀寫(xiě)的機(jī)制。該目的通過(guò)將代碼切片成一個(gè)一個(gè)的臨界區(qū)而達(dá)成段标,如上面的@ synchronized涯冠、NSLock 、pthread_mutex逼庞。

  • 條件鎖
    條件鎖就是條件變量蛇更,當(dāng)進(jìn)程的某些資源要求不滿(mǎn)足時(shí)就進(jìn)入休眠,即鎖住了赛糟,當(dāng)資源被分配到了派任,條件鎖打開(kāi)了,進(jìn)程繼續(xù)運(yùn)行璧南,如上面的NSCondition掌逛、NSConditionLock

  • 遞歸鎖
    遞歸鎖就是同一個(gè)線(xiàn)程可以加鎖 N 次而不會(huì)引發(fā)死鎖。遞歸鎖是特殊的互斥鎖司倚,即帶有遞歸性質(zhì)的互斥鎖豆混,如:pthread_mutex(recursive)NSRecursiveLock动知。

  • 讀寫(xiě)鎖:
    是計(jì)算機(jī)程序的并發(fā)控制的一種同步機(jī)制皿伺,也稱(chēng)“共享-互斥鎖”或者“多讀-單寫(xiě)鎖”陆赋,是一種特殊的自旋鎖市咽。用于解決多線(xiàn)程對(duì)公共資源讀寫(xiě)問(wèn)題。讀操作可并發(fā)重入萄凤,寫(xiě)操作是互斥的丹皱。讀寫(xiě)鎖通常用互斥鎖妒穴、條件變量、信號(hào)量實(shí)現(xiàn)种呐。

  • 信號(hào)量(semaphore):
    是一種更高級(jí)的同步機(jī)制宰翅,互斥鎖可以說(shuō)是semaphore在僅取值0/1時(shí)的特例弃甥。信號(hào)量可以有更多的取值空間爽室,用來(lái)實(shí)現(xiàn)更加復(fù)雜的同步,而不單單是線(xiàn)程間互斥,如:dispatch_semaphore阔墩。

其實(shí)基本的鎖就包括三類(lèi):自旋鎖嘿架、互斥鎖、讀寫(xiě)鎖啸箫,其他的比如條件鎖耸彪、遞歸鎖、信號(hào)量都是上層的封裝和實(shí)現(xiàn)忘苛。

3.1 OSSpinLock

OSSpinLock叫做“自旋鎖”蝉娜,需要導(dǎo)入頭文件

#import <libkern/OSAtomic.h>

常用的API:

OSSpinLock lock = OS_UNFAIR_LOCK_INIT; ——初始化鎖對(duì)象lock
OSSpinLockTry(&lock);——嘗試加鎖,加鎖成功繼續(xù)扎唾,加鎖失敗返回召川,繼續(xù)執(zhí)行后面的代碼,不阻塞線(xiàn)程
OSSpinLockLock(&lock);——加鎖胸遇,加鎖失敗會(huì)阻塞線(xiàn)程荧呐,進(jìn)行等待
OSSpinLockUnlock(&lock);——解鎖

我們使用上面的第二個(gè)賣(mài)票示例來(lái)進(jìn)行加鎖操作:

#import <libkern/OSAtomic.h>
@interface ViewController () 
// 票總數(shù)
@property (nonatomic, assign) NSInteger ticketNum;
// 鎖
@property (nonatomic, assign) OSSpinLock lock;
@end

- (void)ticketTest{
    
    // 初始化鎖
    self.lock = OS_SPINLOCK_INIT;
    self.ticketNum = 30;

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

// 賣(mài)票操作
- (void)saleTicket{
    
    // 加鎖????
    OSSpinLockLock(&_lock);
    
    self.ticketNum -= 1;
    NSLog(@"還剩%ld張票,currentThread - %@",self.ticketNum,[NSThread currentThread]);
    
    // 解鎖????
    OSSpinLockUnlock(&_lock);
}

結(jié)果輸出:

2019-10-12 17:02:55.774748+0800 tianran[16628:4429441] 還剩29張票纸镊,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}
2019-10-12 17:02:55.774955+0800 tianran[16628:4429445] 還剩28張票倍阐,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775065+0800 tianran[16628:4429445] 還剩27張票,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775156+0800 tianran[16628:4429443] 還剩26張票逗威,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775237+0800 tianran[16628:4429443] 還剩25張票峰搪,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775301+0800 tianran[16628:4429443] 還剩24張票,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775368+0800 tianran[16628:4429443] 還剩23張票庵楷,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775433+0800 tianran[16628:4429443] 還剩22張票罢艾,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775513+0800 tianran[16628:4429445] 還剩21張票,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775578+0800 tianran[16628:4429445] 還剩20張票尽纽,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775643+0800 tianran[16628:4429445] 還剩19張票咐蚯,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775717+0800 tianran[16628:4429441] 還剩18張票,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}
2019-10-12 17:02:55.775781+0800 tianran[16628:4429441] 還剩17張票弄贿,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}
2019-10-12 17:02:55.775911+0800 tianran[16628:4429441] 還剩16張票春锋,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}
2019-10-12 17:02:55.775983+0800 tianran[16628:4429441] 還剩15張票,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}

這里要注意差凹,我們使用的是同一個(gè)鎖對(duì)象期奔,如果用多個(gè),肯定也沒(méi)效果啊危尿。

賣(mài)票的問(wèn)題呐萌,我們針對(duì)的是同一個(gè)操作來(lái)處理的,而存錢(qián)取錢(qián)的問(wèn)題谊娇,涉及到了兩個(gè)操作(存錢(qián)和取錢(qián))肺孤,首先要明確問(wèn)題,加鎖機(jī)制是為了解決多個(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn)共享資源所產(chǎn)生的數(shù)據(jù)問(wèn)題,無(wú)論這些線(xiàn)程里面執(zhí)行的是什么操作赠堵,如果這些操作影響了共享資源小渊,不能同時(shí)進(jìn)行的話(huà),那么應(yīng)該對(duì)這些操作使用同一個(gè)鎖對(duì)象進(jìn)行加鎖茫叭。
所以酬屉,我們應(yīng)該對(duì)存錢(qián)操作取錢(qián)操作使用相同的鎖對(duì)象進(jìn)行加鎖。
代碼如下:

#import <libkern/OSAtomic.h>
@interface ViewController () 
// 總錢(qián)數(shù)
@property (nonatomic, assign) NSInteger money;
// 鎖
@property (nonatomic, assign) OSSpinLock lock;
@end

- (void)moneyTest{
    
    // 初始化鎖
    self.lock = OS_SPINLOCK_INIT;
    self.money = 1000;

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 10; i++) {
            [self saveMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 10; i++) {
            [self drawMoney];
        }
    });
}

// 存錢(qián)
- (void)saveMoney{
    sleep(0.5);
    OSSpinLockLock(&_lock);
    NSInteger oldMoney = self.money;
    oldMoney += 50;
    self.money = oldMoney;
    NSLog(@"存了50元揍愁,賬戶(hù)余額 %ld呐萨,當(dāng)前線(xiàn)程 %@",self.money,[NSThread currentThread]);
    OSSpinLockUnlock(&_lock);
}

// 取錢(qián)
- (void)drawMoney{
    sleep(0.5);
    OSSpinLockLock(&_lock);
    NSInteger oldMoney = self.money;
    oldMoney -= 20;
    self.money = oldMoney;
    NSLog(@"取了20元,賬戶(hù)余額 %ld莽囤,當(dāng)前線(xiàn)程 %@",self.money,[NSThread currentThread]);
    OSSpinLockUnlock(&_lock);
}

輸出結(jié)果:

2019-10-12 17:14:44.362398+0800 tianran[16653:4433835] 存了50元垛吗,賬戶(hù)余額 1050,當(dāng)前線(xiàn)程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.362540+0800 tianran[16653:4433832] 取了20元烁登,賬戶(hù)余額 1030怯屉,當(dāng)前線(xiàn)程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.362640+0800 tianran[16653:4433835] 存了50元,賬戶(hù)余額 1080饵沧,當(dāng)前線(xiàn)程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.362711+0800 tianran[16653:4433832] 取了20元锨络,賬戶(hù)余額 1060,當(dāng)前線(xiàn)程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.362790+0800 tianran[16653:4433835] 存了50元狼牺,賬戶(hù)余額 1110羡儿,當(dāng)前線(xiàn)程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.362923+0800 tianran[16653:4433832] 取了20元,賬戶(hù)余額 1090是钥,當(dāng)前線(xiàn)程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363015+0800 tianran[16653:4433835] 存了50元掠归,賬戶(hù)余額 1140,當(dāng)前線(xiàn)程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363078+0800 tianran[16653:4433832] 取了20元悄泥,賬戶(hù)余額 1120虏冻,當(dāng)前線(xiàn)程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363152+0800 tianran[16653:4433835] 存了50元,賬戶(hù)余額 1170弹囚,當(dāng)前線(xiàn)程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363206+0800 tianran[16653:4433832] 取了20元厨相,賬戶(hù)余額 1150,當(dāng)前線(xiàn)程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363278+0800 tianran[16653:4433835] 存了50元鸥鹉,賬戶(hù)余額 1200蛮穿,當(dāng)前線(xiàn)程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363335+0800 tianran[16653:4433832] 取了20元,賬戶(hù)余額 1180毁渗,當(dāng)前線(xiàn)程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363405+0800 tianran[16653:4433835] 存了50元践磅,賬戶(hù)余額 1230,當(dāng)前線(xiàn)程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363458+0800 tianran[16653:4433832] 取了20元灸异,賬戶(hù)余額 1210府适,當(dāng)前線(xiàn)程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363555+0800 tianran[16653:4433835] 存了50元幻碱,賬戶(hù)余額 1260,當(dāng)前線(xiàn)程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363723+0800 tianran[16653:4433835] 存了50元细溅,賬戶(hù)余額 1310,當(dāng)前線(xiàn)程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363770+0800 tianran[16653:4433835] 存了50元儡嘶,賬戶(hù)余額 1360喇聊,當(dāng)前線(xiàn)程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363889+0800 tianran[16653:4433832] 取了20元,賬戶(hù)余額 1340蹦狂,當(dāng)前線(xiàn)程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.364014+0800 tianran[16653:4433832] 取了20元誓篱,賬戶(hù)余額 1320,當(dāng)前線(xiàn)程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.364129+0800 tianran[16653:4433832] 取了20元凯楔,賬戶(hù)余額 1300窜骄,當(dāng)前線(xiàn)程 <NSThread: 0x281189080>{number = 3, name = (null)}

這次就沒(méi)問(wèn)題了,過(guò)程和結(jié)果都沒(méi)有問(wèn)題摆屯。

上面有提到邻遏,OSSpinLock自旋鎖,那么為什么叫做自旋鎖呢虐骑?
自旋鎖的原理是當(dāng)已經(jīng)被別的線(xiàn)程加鎖了准验,加鎖失敗的時(shí)候,讓線(xiàn)程處于忙等的狀態(tài)廷没,以此讓線(xiàn)程停留在臨界區(qū)(需要加鎖的代碼段)之外糊饱,一旦加鎖成功,線(xiàn)程便可以進(jìn)入臨界區(qū)進(jìn)行對(duì)共享資源操作颠黎。

讓線(xiàn)程阻塞有兩種方法:

  • 讓線(xiàn)程休眠另锋,RunLoop里面用到的mach_msg()實(shí)現(xiàn)的效果就是這一種,它借助系統(tǒng)內(nèi)核指令狭归,??線(xiàn)程停下來(lái)夭坪,CPU不再分配資源給線(xiàn)程,因此不會(huì)再執(zhí)行任何一句匯編指令过椎,我們后面要介紹的互斥所台舱,也是屬于這種,它的底層匯編調(diào)用了一個(gè)系統(tǒng)函數(shù)syscall使得線(xiàn)程進(jìn)入休眠
  • 自旋鎖的忙等潭流,本質(zhì)上是一個(gè)while循環(huán)竞惋,不斷的去判斷加鎖條件,一旦當(dāng)前已經(jīng)進(jìn)入臨界區(qū)(加鎖代碼塊)的線(xiàn)程完成了操作灰嫉,解開(kāi)鎖之后拆宛,等待鎖的線(xiàn)程便可以成功加鎖,再次進(jìn)入臨界區(qū)讼撒。自旋鎖其實(shí)并沒(méi)有真正讓線(xiàn)程停下來(lái)股耽,線(xiàn)程只不過(guò)是暫時(shí)被困在while循環(huán)里面,CPU還是在不斷的分配資源去處理它的匯編指令的(while循環(huán)的匯編指令)钳幅。

現(xiàn)在蘋(píng)果已經(jīng)建議開(kāi)發(fā)者停止使用OSSpinLock了物蝙,在線(xiàn)程優(yōu)先級(jí)的作用下,會(huì)產(chǎn)生【優(yōu)先級(jí)反轉(zhuǎn)】敢艰,使得自旋鎖卡住诬乞,因此它不再安全了。
可以看不再安全的OSSpinLock钠导。

我們知道震嫉,計(jì)算機(jī)的CPU在同一時(shí)間,只能處理一條線(xiàn)程牡属,對(duì)于單核``CPU來(lái)說(shuō)票堵,線(xiàn)程的并發(fā),實(shí)際上是一種假象逮栅,是系統(tǒng)讓CPU一很小的時(shí)間間隔在線(xiàn)程之間來(lái)回切換悴势,所以看上去多條線(xiàn)程好像是在同時(shí)進(jìn)行的。到了多核CPU時(shí)代措伐,確實(shí)可以實(shí)現(xiàn)真正的線(xiàn)程并發(fā)瞳浦,但是CPU核心數(shù)畢竟是有限的,而程序內(nèi)部的線(xiàn)程數(shù)量通撤鲜浚肯定是遠(yuǎn)大于CPU數(shù)量的叫潦,因此,很多情況下我們面對(duì)的還是單CPU處理多線(xiàn)程的情況官硝〈H铮基于這種場(chǎng)景,需要了解一個(gè)概念叫做線(xiàn)程優(yōu)先級(jí)氢架,CPU會(huì)將盡可能多的時(shí)間(資源)分配給優(yōu)先級(jí)高的線(xiàn)程傻咖,我們用下圖來(lái)展示一下所謂的優(yōu)先級(jí)反轉(zhuǎn)問(wèn)題:

image.png

低優(yōu)先級(jí)線(xiàn)程A獲得鎖并訪(fǎng)問(wèn)共享資源,這時(shí)一個(gè)高優(yōu)先級(jí)線(xiàn)程B也嘗試獲得這個(gè)鎖岖研,它會(huì)處于spin lock的忙等狀態(tài)從而占用大量CPU卿操,此時(shí)低優(yōu)先級(jí)的線(xiàn)程A無(wú)法與高優(yōu)先級(jí)線(xiàn)程B爭(zhēng)奪CPU時(shí)間,從而導(dǎo)致線(xiàn)程A任務(wù)遲遲完不成孙援,無(wú)法釋放lock害淤,所以會(huì)出現(xiàn)卡住的問(wèn)題。

自旋鎖的while循環(huán)本質(zhì)拓售,使得線(xiàn)程并沒(méi)有停下來(lái)窥摄,一般情況下,一條線(xiàn)程等待鎖的時(shí)間不會(huì)太長(zhǎng)础淤,選用自旋鎖來(lái)阻塞線(xiàn)程所消耗的CPU資源崭放,要小于線(xiàn)程的休眠和喚醒所帶來(lái)的CPU資源開(kāi)銷(xiāo)哨苛,因此自選鎖是一種效率很高的加鎖機(jī)制,但是優(yōu)先級(jí)反轉(zhuǎn)問(wèn)題使得自旋鎖不再安全币砂,鎖的最終目的是安全不是效率建峭,所以蘋(píng)果放棄了OSSpinLock

iOS 10/macOS 10.12發(fā)布時(shí)决摧,蘋(píng)果提供了新的 os_unfair_lock 作為 OSSpinLock 的替代亿蒸,并且將OSSpinLock 標(biāo)記為了 Deprecated

另外為什么RunLoop要選擇真正的線(xiàn)程休眠呢蜜徽?因?yàn)閷?duì)于A(yíng)pp來(lái)說(shuō),可能處于長(zhǎng)時(shí)間的擱置狀態(tài)票摇,而沒(méi)有任何用戶(hù)行為發(fā)生拘鞋,不需要CPU管,對(duì)于這種場(chǎng)景矢门,當(dāng)然是讓線(xiàn)程休眠更為節(jié)約性能盆色。

3.2 os_unfair_lock

蘋(píng)果建議從iOS10.0之后,使用os_unfair_lock代替OSSpinLock祟剔,現(xiàn)在我們就去看一下如何使用:
先導(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í)行后面的代碼宣旱,不阻塞線(xiàn)程
os_unfair_lock_lock(&lock);
加鎖,加鎖失敗會(huì)阻塞線(xiàn)程進(jìn)行等待
os_unfair_lock_unlock(&lock);
解鎖

它的使用和OSSpinLock的方法一樣叛薯,這里就不再舉例了浑吟,蘋(píng)果為了解決OSSpinLock的優(yōu)先級(jí)反轉(zhuǎn)問(wèn)題,在os_unfair_lock中摒棄了忙等方式耗溜,使用讓線(xiàn)程真正休眠的方式组力,來(lái)阻塞線(xiàn)程,也就從根本上解決了問(wèn)題抖拴。

3.3 atomic

atomic適用于OC中屬性的修飾符燎字,其自帶一把互斥鎖,但是這個(gè)一般基本不使用阿宅,都是使用的nonatomic候衍。

setter 方法會(huì)根據(jù)修飾符調(diào)用不同的方法,其中最后會(huì)統(tǒng)一調(diào)用 reallySetProperty方法洒放,其中就有對(duì)于atomic非 atomic 的操作:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
   ...
   id *slot = (id*) ((char*)self + offset);
   ...

    if (!atomic) {//未加鎖
        oldValue = *slot;
        *slot = newValue;
    } else {//加鎖
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    ...
}

從源碼中可以看出脱柱,對(duì)于atomic修飾的屬性,進(jìn)行了spinlock_t加鎖處理拉馋,但是在前文中提到OSSpinLock已經(jīng)廢棄了榨为,這里的spinlock_t在底層是通過(guò)os_unfair_lock互斥鎖替代了OSSpinLock自旋鎖:

using spinlock_t = mutex_tt<LOCKDEBUG>;

class mutex_tt : nocopy_t {
    os_unfair_lock mLock;
    ...
}
3.4 pthread_mutex

pthread_mutex來(lái)自pthread惨好,是一個(gè)跨平臺(tái)的解決方案,mutex意為互斥鎖随闺,等待鎖的線(xiàn)程會(huì)處于休眠狀態(tài)日川,它有如下API:

需要先導(dǎo)入 #import <pthread.h>

初始化鎖的屬性
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);
銷(xiāo)毀相關(guān)資源
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&attr);

我們先介紹一下pthread_mutex的初始化方法:

int pthread_mutex_init(pthread_mutex_t * __restrict,
        const pthread_mutexattr_t * _Nullable __restrict);
  • pthread_mutex_t * __restrict
    要進(jìn)行初始化的鎖對(duì)象
  • const pthread_mutexattr_t * _Nullable __restrict
    鎖對(duì)象的屬性

由于第二個(gè)參數(shù)是鎖對(duì)象的屬性,所以我們還需要專(zhuān)門(mén)生成屬性對(duì)象矩乐,通過(guò) 定義屬性對(duì)象 -> 初始化屬性對(duì)象 -> 設(shè)置屬性種類(lèi) 3步來(lái)完成龄句,屬性的類(lèi)別有以下幾類(lèi):

#define PTHREAD_MUTEX_NORMAL        0 // 普通互斥鎖
#define PTHREAD_MUTEX_ERRORCHECK    1 // 檢查錯(cuò)誤鎖,不常用
#define PTHREAD_MUTEX_RECURSIVE     2 // 遞歸互斥鎖散罕,下面會(huì)有介紹
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL

如果我們給鎖設(shè)定默認(rèn)屬性分歇,那么可以用以下代碼進(jìn)行鎖的初始化,不用再配置屬性信息欧漱,其中參數(shù)NULL表示的就是初始化一個(gè)普通的互斥鎖

pthread_mutex_init(mutex, NULL);

好了职抡,我們先用賣(mài)票示例來(lái)使用一下pthread_mutex,代碼如下:

#import <pthread.h>
@interface ViewController () 
@property (nonatomic, assign) NSInteger ticketNum;
@property (nonatomic, assign) pthread_mutex_t pthread_lock;
@end

- (void)ticketTest{
    
    // 初始化鎖的屬性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    
    // 初始化鎖
    pthread_mutex_init(&_pthread_lock, &attr);
    
    self.ticketNum = 30;

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

// 賣(mài)票操作
- (void)saleTicket{
    
    // 加鎖????
    pthread_mutex_lock(&_pthread_lock);
    
    self.ticketNum -= 1;
    NSLog(@"還剩%ld張票误甚,currentThread - %@",self.ticketNum,[NSThread currentThread]);
    
    // 解鎖????
    pthread_mutex_unlock(&_pthread_lock);
}

-(void)dealloc{
    pthread_mutex_destroy(&_pthread_lock);
}

輸出結(jié)果就不貼了缚甩,是沒(méi)有問(wèn)題的,太占地方了窑邦。

3.5 互斥遞歸鎖

請(qǐng)看下面的代碼場(chǎng)景:

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

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

如果正常調(diào)用 otherTest 這個(gè)方法擅威,結(jié)果如下:

[ViewController otherTest]
[ViewController otherTest2]

如果這兩段代碼都需要保證線(xiàn)程安全,我們通過(guò)加互斥鎖冈钦,來(lái)看下效果:

-(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é)果:

-[ViewController otherTest]

像上面的代碼一樣郊丛,兩個(gè)方法都加上同一把鎖,可以看到調(diào)用otherTest方法會(huì)導(dǎo)致線(xiàn)程卡在該方法里面瞧筛,只完成了打印代碼的執(zhí)行宾袜,就不繼續(xù)往下走了,為啥呢驾窟?看下圖:

死鎖

我們可以從上面代碼中看到庆猫,在otherTest方法中,先加鎖了绅络, [self otherTest2] 在加鎖之后調(diào)用月培,但是otherTest2方法中,此時(shí)還是加鎖的狀態(tài)恩急,所以otherTest2方法一直不執(zhí)行杉畜,由于otherTest方法中,解鎖操作是在otherTest2方法執(zhí)行之后衷恭,所以造成死鎖此叠。

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

但是灭袁,如果在開(kāi)發(fā)中如果碰到需要給 遞歸函數(shù) 加鎖猬错,如下面所示:

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

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

這樣就無(wú)法通過(guò)不同的鎖對(duì)象來(lái)加鎖了,只要使用相同的鎖對(duì)象茸歧,就會(huì)出現(xiàn)死鎖倦炒。針對(duì)這種情況,ptherad給我們提供了遞歸鎖來(lái)解決這個(gè)問(wèn)題软瞎。要想使用遞歸鎖逢唤,我們只需要在初始化屬性的時(shí)候,選擇遞歸鎖屬性即可涤浇,其他的使用步驟跟普通互斥鎖沒(méi)有區(qū)別鳖藕,如下:

pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_RECURSIVE);

那么遞歸鎖是如何避免死鎖的呢?對(duì)于同一個(gè)鎖對(duì)象來(lái)說(shuō)只锭,允許重復(fù)的加鎖著恩,重復(fù)的解鎖,因?yàn)閷?duì)于一個(gè)有出口的遞歸函數(shù)來(lái)說(shuō):
函數(shù)的調(diào)用次數(shù) = 函數(shù)的退出次數(shù)
因此加鎖次數(shù)pthread_mutex_lock和解鎖的次數(shù)pthread_mutex_unlock是相等的纹烹,所以遞歸函數(shù)結(jié)束時(shí)页滚,所有的鎖都會(huì)被解開(kāi)召边。

但是遞歸鎖只是針對(duì)在同一個(gè)線(xiàn)程里面可以重復(fù)加鎖和解鎖铺呵。

3.6 互斥條件鎖 pthread_cond_t
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è)等待該條件的線(xiàn)程
pthread_cond_broadcast(&condition); 激活所有等待條件的線(xiàn)程
pthread_mutex_destroy(&mutex); 銷(xiāo)毀鎖對(duì)象
pthread_cond_destroy(&condition); 銷(xiāo)毀條件對(duì)象

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

  • 在 remove 方法里對(duì)數(shù)組 dataArr 進(jìn)行刪除元素操作
  • 在 add 方法里面對(duì)dataArr 進(jìn)行元素添加操作
  • 并且要求隧熙,如果 dataArr 的元素個(gè)數(shù)為0片挂,則不能進(jìn)行刪除操作
    代碼如下:
@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *dataArr;
// 鎖對(duì)象
@property (nonatomic, assign) pthread_mutex_t pthread_lock;
// 條件對(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(&_pthread_lock, &_attr);
    // 初始化條件
    pthread_cond_init(&_cond, NULL);
    pthread_mutexattr_destroy(&_attr);
    // 初始化數(shù)組
    self.dataArr = [NSMutableArray array];
    
    [self dataArrTest];
}

- (void)dataArrTest{
    // 開(kāi)啟remove 操作,此時(shí)dataArr為空
    NSThread *removeThread = [[NSThread alloc] initWithTarget:self selector:@selector(remove) object:nil];
    [removeThread setName:@"RemoveThread"];
    [removeThread start];
    
    // 開(kāi)啟add操作
    NSThread *addThread = [[NSThread alloc] initWithTarget:self selector:@selector(add) object:nil];
    [addThread setName:@"AddThread"];
    [addThread start];
}

// 移除元素
- (void)remove{
    // 加鎖
    pthread_mutex_lock(&_pthread_lock);
    
    NSLog(@"加鎖成功贞盯,線(xiàn)程:%@音念,remove操作開(kāi)始",[NSThread currentThread]);
    
    if (!self.dataArr.count) {
        // 進(jìn)行條件等待
        NSLog(@"dataArr沒(méi)有元素,開(kāi)始等待");
        pthread_cond_wait(&_cond, &_pthread_lock);
        NSLog(@"接收到條件更新信號(hào)躏敢,dataArr有了元素闷愤,繼續(xù)remove操作");
    }
    [self.dataArr removeLastObject];
    NSLog(@"remove成功,dataArr剩余元素個(gè)數(shù):%ld",self.dataArr.count);
    
    // 解鎖
    pthread_mutex_unlock(&_pthread_lock);
    NSLog(@"remove 解鎖成功件余,線(xiàn)程:%@讥脐,線(xiàn)程結(jié)束\n",[NSThread currentThread]);
}

// 添加元素
- (void)add{
    // 加鎖
    pthread_mutex_lock(&_pthread_lock);
    
    NSLog(@"加鎖成功,線(xiàn)程:%@啼器,add操作開(kāi)始",[NSThread currentThread]);
    sleep(2);
    [self.dataArr addObject:@"111"];
    NSLog(@"add成功旬渠,dataArr剩余元素個(gè)數(shù):%ld,發(fā)送條件信號(hào)",self.dataArr.count);
    
    // 發(fā)送條件信號(hào)
    pthread_cond_signal(&_cond);
    
    // 解鎖
    pthread_mutex_unlock(&_pthread_lock);
    NSLog(@"add 解鎖成功端壳,線(xiàn)程:%@告丢,線(xiàn)程結(jié)束",[NSThread currentThread]);
}

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

輸出結(jié)果:

2019-10-14 15:14:52.929491+0800 tianran[2004:431723] 加鎖成功,線(xiàn)程:<NSThread: 0x283f8cd40>{number = 6, name = RemoveThread}损谦,remove操作開(kāi)始
2019-10-14 15:14:52.929557+0800 tianran[2004:431723] dataArr沒(méi)有元素岖免,開(kāi)始等待
2019-10-14 15:14:52.929641+0800 tianran[2004:431724] 加鎖成功岳颇,線(xiàn)程:<NSThread: 0x283f8c9c0>{number = 7, name = AddThread},add操作開(kāi)始
2019-10-14 15:14:54.935014+0800 tianran[2004:431724] add成功觅捆,dataArr剩余元素個(gè)數(shù):1赦役,發(fā)送條件信號(hào)
2019-10-14 15:14:54.935672+0800 tianran[2004:431723] 接收到條件更新信號(hào),dataArr有了元素栅炒,繼續(xù)remove操作
2019-10-14 15:14:54.935790+0800 tianran[2004:431724] add 解鎖成功掂摔,線(xiàn)程:<NSThread: 0x283f8c9c0>{number = 7, name = AddThread},線(xiàn)程結(jié)束
2019-10-14 15:14:54.936061+0800 tianran[2004:431723] remove成功赢赊,dataArr剩余元素個(gè)數(shù):0
2019-10-14 15:14:54.936415+0800 tianran[2004:431723] remove 解鎖成功乙漓,線(xiàn)程:<NSThread: 0x283f8cd40>{number = 6, name = RemoveThread},線(xiàn)程結(jié)束

從案例以及運(yùn)行結(jié)果分析释移,互斥鎖的條件pthread_cond_t可以在線(xiàn)程加鎖之后叭披,如果條件不達(dá)標(biāo),暫停線(xiàn)程玩讳,等到條件符合標(biāo)準(zhǔn)涩蜘,繼續(xù)執(zhí)行線(xiàn)程,請(qǐng)看下圖:


image.png

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

  • 首先在A(yíng)線(xiàn)程內(nèi)碰到業(yè)務(wù)邏輯無(wú)法往下執(zhí)行的時(shí)候熏纯,調(diào)用pthread_cond_wait(),這句代碼首先會(huì)解鎖當(dāng)前線(xiàn)程同诫,然后休眠當(dāng)前線(xiàn)程以等待條件信號(hào)
  • 此時(shí),鎖已經(jīng)解開(kāi)樟澜,那么值錢(qián)等待鎖的B線(xiàn)程可以成功加鎖误窖,執(zhí)行它后面的邏輯,由于B線(xiàn)程內(nèi)的某些操作完成后可以出發(fā)A的運(yùn)行條件秩贰,此時(shí)從B線(xiàn)程通過(guò)pthread_cond_signal(&_cond)向外發(fā)出條件信號(hào)
  • A線(xiàn)程的收到了條件信號(hào)就會(huì)被pthread_cond_t喚醒霹俺,一旦B線(xiàn)程解鎖之后,pthread_cond_t會(huì)在A(yíng)線(xiàn)程內(nèi)重新加鎖毒费,繼續(xù)A線(xiàn)程的后續(xù)操作丙唧,并最終解鎖。從前到后觅玻,有三次加鎖想际,三次解鎖
  • 通過(guò)pthread_cond_t就實(shí)現(xiàn)了一種線(xiàn)程與線(xiàn)程之間的依賴(lài)關(guān)系,實(shí)際開(kāi)發(fā)中我們會(huì)有不少場(chǎng)景需要用到這種跨線(xiàn)程依賴(lài)關(guān)系
3.7 NSLock NSRecursiveLock NSCondition

上面我們了解的 mutex普通鎖串塑,mutex遞歸鎖沼琉,mutex條件鎖,都是基于C語(yǔ)言的API桩匪,蘋(píng)果在此基礎(chǔ)上打瘪,進(jìn)行了一層面向?qū)ο蟮姆庋b:

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

由于底層是pthread_mutex,這里不再通過(guò)代碼案例演示,下面列舉一下相關(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];
3.8 NSConditionLock

蘋(píng)果基于NSCondition又進(jìn)一步封裝了NSConditionLock闺骚,該鎖允許我們?cè)阪i中設(shè)定條件具體條件值彩扔,有了這個(gè)功能,我們可以更加方便的管理多條線(xiàn)程的依賴(lài)關(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;

接下來(lái)通過(guò)案例來(lái)說(shuō)明它的功能:

@interface ViewController ()

@property (nonatomic, strong) NSConditionLock *conditionLock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.conditionLock = [[NSConditionLock alloc] init];
    
    [self dataArrTest];
}

- (void)dataArrTest{

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        [self.conditionLock lock];
        NSLog(@"__one");
        sleep(1);
        [self.conditionLock unlockWithCondition:2];
    });
    
    dispatch_async(queue, ^{
        [self.conditionLock lockWhenCondition:2];
        NSLog(@"__two");
        sleep(1);
        [self.conditionLock unlockWithCondition:3];
    });
    
    dispatch_async(queue, ^{
        [self.conditionLock lockWhenCondition:3];
        NSLog(@"__three");
        sleep(1);
        [self.conditionLock unlock];
    });
}
@end

代碼輸出:

__one __two __three

全局并發(fā)隊(duì)列虫碉,異步操作,這里就是使用NSConditionLock達(dá)到了線(xiàn)程依賴(lài)的功能胸梆,使用NSOperation也可以敦捧,我們用圖說(shuō)明一下:


image.png
3.9 dispatch_semaphore

GCD提供了dispatch_semaphore方案來(lái)處理多線(xiàn)程同步問(wèn)題。

3.10 @synchronized

這個(gè)使用很簡(jiǎn)單碰镜,也比較常見(jiàn):

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

但是它是所有線(xiàn)程同步方案里面性能最差的兢卵。

開(kāi)啟匯編調(diào)試,發(fā)現(xiàn)@synchronized在執(zhí)行過(guò)程中绪颖,會(huì)走底層的 objc_sync_enterobjc_sync_exit方法:

image.png

也可以通過(guò) clang秽荤,查看底層編譯代碼:


image.jpeg

通過(guò)對(duì)objc_sync_enter方法進(jìn)行符號(hào)斷點(diǎn),查看底層所在的源碼庫(kù)柠横,通過(guò)斷點(diǎn)發(fā)現(xiàn)在objc 源碼中窃款,進(jìn)入objc_sync_enter的源碼實(shí)現(xiàn):

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

    if (obj) {//傳入不為nil
        SyncData* data = id2data(obj, ACQUIRE);//重點(diǎn)
        ASSERT(data);
        data->mutex.lock();//加鎖
    } else {//傳入nil
        // @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;
}

主要操作是判斷 obj 有值,然后通過(guò)id2data獲取了相應(yīng)的 SyncData牍氛,然后進(jìn)行了加鎖操作晨继。

我們?cè)倏聪?code>objc_sync_exit源碼實(shí)現(xiàn):

// End synchronizing on 'obj'. 結(jié)束對(duì)“ obj”的同步
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {//obj不為nil
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();//解鎖
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {//obj為nil時(shí),什么也不做
        // @synchronized(nil) does nothing
    }
    return result;
}

這個(gè)方法在獲取了對(duì)應(yīng)的 SyncData 之后糜俗,進(jìn)行了解鎖操作踱稍。

通過(guò)上面兩個(gè)實(shí)現(xiàn)邏輯發(fā)現(xiàn)曲饱,在 obj 存在時(shí)悠抹,都會(huì)通過(guò)id2data方法,獲取SyncData扩淀。

進(jìn)入 SyncData楔敌,是一個(gè)結(jié)構(gòu)體:

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

其內(nèi)部有一個(gè)結(jié)構(gòu)體的指針struct SyncData* nextData,類(lèi)似于鏈表結(jié)構(gòu)驻谆,有 next指向卵凑,并且封裝了recursive_mutex_t mutex,確認(rèn)@synchronized是一個(gè)遞歸互斥鎖胜臊。

進(jìn)入SyncCache定義勺卢,也是一個(gè)結(jié)構(gòu)體,用于存儲(chǔ)線(xiàn)程象对,其中 list[0]表示當(dāng)前線(xiàn)程的鏈表 data黑忱,主要用于存儲(chǔ) SyncData 和 lockCount:

typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0];
} SyncCache;

我們現(xiàn)在來(lái)看下主要的id2data方法,這個(gè)是加鎖和解鎖都會(huì)調(diào)用的方法:

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;

#if SUPPORT_DIRECT_THREAD_KEYS //tls(Thread Local Storage,本地局部的線(xiàn)程緩存)
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    //通過(guò)KVC方式對(duì)線(xiàn)程進(jìn)行獲取 線(xiàn)程綁定的data
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    //如果線(xiàn)程緩存中有data甫煞,執(zhí)行if流程
    if (data) {
        fastCacheOccupied = YES;
        //如果在線(xiàn)程空間找到了data
        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            //通過(guò)KVC獲取lockCount菇曲,lockCount用來(lái)記錄 被鎖了幾次,即 該鎖可嵌套
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {
                //objc_sync_enter走這里抚吠,傳入的是ACQUIRE -- 獲取
                lockCount++;//通過(guò)lockCount判斷被鎖了幾次常潮,即表示 可重入(遞歸鎖如果可重入,會(huì)死鎖)
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);//設(shè)置
                break;
            }
            case RELEASE:
                //objc_sync_exit走這里楷力,傳入的why是RELEASE -- 釋放
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // remove from fast cache
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif

    // Check per-thread cache of already-owned locks for matching object
    SyncCache *cache = fetch_cache(NO);//判斷緩存中是否有該線(xiàn)程
    //如果cache中有喊式,方式與線(xiàn)程緩存一致
    if (cache) {
        unsigned int I;
        for (i = 0; i < cache->used; i++) {//遍歷總表
            SyncCacheItem *item = &cache->list[I];
            if (item->data->object != object) continue;

            // Found a match.
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE://加鎖
                item->lockCount++;
                break;
            case RELEASE://解鎖
                item->lockCount--;
                if (item->lockCount == 0) {
                    // remove from per-thread cache 從cache中清除使用標(biāo)記
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }

    // Thread cache didn't find anything.
    // Walk in-use list looking for matching object
    // Spinlock prevents multiple threads from creating multiple 
    // locks for the same new object.
    // We could keep the nodes in some hash table if we find that there are
    // more than 20 or so distinct locks active, but we don't do that now.
    //第一次進(jìn)來(lái),所有緩存都找不到
    lockp->lock();

    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {//cache中已經(jīng)找到
            if ( p->object == object ) {//如果不等于空萧朝,且與object相似
                result = p;//賦值
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);//對(duì)threadCount進(jìn)行++
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object 沒(méi)有與當(dāng)前對(duì)象關(guān)聯(lián)的SyncData
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it 第一次進(jìn)來(lái)垃帅,沒(méi)有找到
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }

    // Allocate a new SyncData and add to list.
    // XXX allocating memory with a global lock held is bad practice,
    // might be worth releasing the lock, allocating, and searching again.
    // But since we never free these guys we won't be stuck in allocation very often.
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));//創(chuàng)建賦值
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock();
    if (result) {
        // Only new ACQUIRE should get here.
        // All RELEASE and CHECK and recursive ACQUIRE are 
        // handled by the per-thread caches above.
        if (why == RELEASE) {
            // Probably some thread is incorrectly exiting 
            // while the object is held by another thread.
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) { //判斷是否支持棧存緩存,支持則通過(guò)KVC形式賦值 存入tls
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);//lockCount = 1
        } else 
#endif
        {
            // Save in thread cache 緩存中存一份
            if (!cache) cache = fetch_cache(YES);//第一次存儲(chǔ)時(shí)剪勿,對(duì)線(xiàn)程進(jìn)行了綁定
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

    return result;
}

第一步:首先在 tls 即線(xiàn)程緩存中查找贸诚。
a.在 tsl_get_direct方法中以線(xiàn)程為key,通過(guò) kvc的方式獲取與之綁定的 SyncData厕吉,即線(xiàn)程 data酱固,其中的tls(),表示本地局部的線(xiàn)程緩存头朱;
b.判斷獲取的data是否存在运悲,以及判斷data 中是否能找到對(duì)應(yīng)的 object;
c.如果都找到了,在tsl_get_direct方法中以 KVC 的方式獲取 lockCount项钮,用來(lái)記錄對(duì)象被鎖了幾次(即鎖的嵌套次數(shù))
d.如果data中的threadCount 小于等于0班眯,或者lockCount 小于等于0時(shí)殴穴,則直接崩潰
e.通過(guò)傳入的why壳鹤,判斷操作類(lèi)型:
e.1 如果是ACQUIRE蝇棉,表示加鎖刃滓,則進(jìn)行lockCount++人柿,并保存到tls緩存
e.2 如果是RELEASE何乎,表示釋放子漩,則進(jìn)行lockCount--幼衰,并保存到tls緩存阿弃。如果lockCount 等于 0诊霹,從tls中移除線(xiàn)程data
e.3 如果是CHECK,則什么也不做

第二步:如果 tls中沒(méi)有渣淳,則在cache緩存中查找
a.通過(guò) fetch_cache方法查找 cache 緩存中是否有線(xiàn)程
b.如果有脾还,則遍歷 cache 總表,讀取線(xiàn)程對(duì)應(yīng)的 SyncCacheItem
c. 從SyncCacheItem中取出data入愧,然后后續(xù)步驟與tls的匹配是一致的

第三步:如果 cache 中也沒(méi)有鄙漏,即第一次進(jìn)來(lái)赛蔫,則創(chuàng)建 SyncData,并存儲(chǔ)到相應(yīng)緩存中:
a.如果在cache中找到線(xiàn)程泥张,且與object相等呵恢,則進(jìn)行賦值、以及threadCount++
b.如果在cache中沒(méi)有找到媚创,則threadCount等于1

針對(duì)tlscache 緩存渗钉,底層的表結(jié)構(gòu)如下:

image.jpeg

  • 哈希表結(jié)構(gòu)中通過(guò)SyncList結(jié)構(gòu)來(lái)組裝多線(xiàn)程的情況
  • SyncData通過(guò)鏈表的形式組裝當(dāng)前可重入的情況
  • 下層通過(guò)tls線(xiàn)程緩存、cache緩存來(lái)進(jìn)行處理
  • 底層主要有兩個(gè)東西:lockCount钞钙、threadCount鳄橘,解決了遞歸互斥鎖,解決了嵌套可重入
3.11 總結(jié)

我們對(duì)iOS的各種線(xiàn)程同步方案體驗(yàn)了一下:

  • OSSpinLock由于其不休眠特性芒炼,所以它的效率是非常高的瘫怜,但是由于安全問(wèn)題,蘋(píng)果建議我們使用os_unfair_lock取而代之本刽,并且效率還要高于前者鲸湃。
  • pthread_mutex是一種跨平臺(tái)的解決方案,性能也不錯(cuò)子寓。當(dāng)然還有蘋(píng)果的GCD解決方案暗挑,也是挺不錯(cuò)的。
  • 對(duì)于NS開(kāi)頭的那些OC下的解決方案斜友,雖然本質(zhì)也還是基于pthread_mutex的封裝炸裆,但是由于多了一些面向?qū)ο蟮牟僮鏖_(kāi)銷(xiāo),效率不免要下降鲜屏。
  • 性能最差的是@synchronized方案烹看,雖然它的使用是最簡(jiǎn)單的,但因?yàn)樗牡讓臃庋b了過(guò)于復(fù)雜的數(shù)據(jù)結(jié)構(gòu)洛史,導(dǎo)致了性能低下惯殊。
    使用推薦如下:
  • os_unfair_lock(推薦??????????)
  • OSSpinLock(不安全????)
  • dispatch_semaphore(推薦??????????)
  • pthread_mutex(推薦????????)
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)(推薦??????)
  • NSLock(??????)
  • NSCondition(??????)
  • pthread_mutex(recursive)(????)
  • NSRecursiveLock(????)
  • NSConditionLock(????)
  • @synchronized(最不推薦)

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

4.1 什么情況下選擇自旋鎖更好?

自旋鎖的特點(diǎn):效率高虹菲、安全性不足靠胜、占用CPU資源大掉瞳,因此選擇自旋鎖依據(jù)原則如下:

  • 預(yù)計(jì)線(xiàn)程等待鎖的時(shí)間很短
  • 加鎖的代碼(臨界區(qū))經(jīng)常被調(diào)用毕源,但是競(jìng)爭(zhēng)的情況發(fā)生概率很小,對(duì)安全性要求不高
  • CPU資源不緊張
  • 多核處理器
4.2 什么情況下選擇互斥鎖更好陕习?

互斥鎖特點(diǎn):安全性突出霎褐、占用CPU資源小,休眠/喚醒過(guò)程要消耗CPU資源该镣,因此選擇互斥鎖依據(jù)原則如下:

  • 預(yù)計(jì)線(xiàn)程等待鎖的時(shí)間比較長(zhǎng)
  • 單核處理器
  • 臨界區(qū)有IO操作
  • 臨界區(qū)代碼復(fù)雜或者循環(huán)量大
  • 臨界區(qū)的競(jìng)爭(zhēng)非常激烈冻璃,對(duì)安全性要求高
4.3 為什么iOS中幾乎不用atomic

atomic是用于保證屬性的setter、getter方法的原子性操作的省艳,本質(zhì)就是在getter和setter內(nèi)部增加線(xiàn)程同步的鎖娘纷,用的鎖實(shí)質(zhì)上也是os_unfair_lock
但是atomic只是保證讀寫(xiě)操作的線(xiàn)程同步跋炕,對(duì)于可變數(shù)組赖晶、可變字典來(lái)說(shuō),如果你使用atomic修飾辐烂,在添加元素的時(shí)候遏插,并不是線(xiàn)程安全的:

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

所以說(shuō)atomic并不能完全保證多線(xiàn)程安全問(wèn)題。
我們幾乎不會(huì)用到atomic纠修,因?yàn)?code>property在iOS代碼中調(diào)用的太頻繁了胳嘲,會(huì)導(dǎo)致鎖的過(guò)度使用,消耗CPU資源扣草,所以我們只需要針對(duì)具體會(huì)出現(xiàn)多線(xiàn)程隱患的地方加鎖就行了了牛,需要加鎖的時(shí)候再去加。

4.4 多線(xiàn)程讀寫(xiě)安全

在上面存取錢(qián)的示例中辰妙,存白魂、和取其實(shí)就是對(duì)共享資源的讀和寫(xiě),假如我們有如下兩個(gè)操作分別只包含讀操作和寫(xiě)操作:

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

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

其實(shí)讀操作的目的上岗,只是取出數(shù)據(jù)福荸,并不會(huì)修改數(shù)據(jù),所以多線(xiàn)程同時(shí)進(jìn)行讀操作是沒(méi)問(wèn)題的肴掷,不需要考慮線(xiàn)程同步的問(wèn)題敬锐,寫(xiě)操作是導(dǎo)致多線(xiàn)程安全問(wèn)題的根本因素。所以為了讀寫(xiě)安全呆瞻,解決方案其實(shí)就是多讀單寫(xiě):

  • 要求1: 同一時(shí)間台夺,只能有1個(gè)線(xiàn)程進(jìn)行寫(xiě)的操作
  • 要求2: 同一時(shí)間,允許有多個(gè)線(xiàn)程進(jìn)行讀的操作
  • 要求3: 同一時(shí)間痴脾,不允許既讀又寫(xiě)颤介,就是說(shuō)讀操作和寫(xiě)操作之間是互斥關(guān)系

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

  1. pthread_rwlock:讀寫(xiě)鎖
  2. dispatch_barrier_async:異步柵欄調(diào)用
  • pthread_rwlock
    使用pthread_rwlock,等待鎖的線(xiàn)程會(huì)進(jìn)入休眠赞赖,API如下:
//初始化鎖
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);
//讀操作加鎖
pthread_rwlock_rdlock(&lock);
//讀操作嘗試加鎖
pthread_rwlock_tryrdlock(&lock);
//寫(xiě)操作加鎖
pthread_rwlock_wrlock(&lock);
//寫(xiě)操作嘗試加鎖
pthread_rwlock_trywrlock(&lock);
//解鎖
pthread_rwlock_unlock(&lock);
//銷(xiāo)毀鎖
pthread_rwlock_destroy(&lock);
  • 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ù)辕近。
    具體介紹,可以看另外一篇文章匿垄,里面有對(duì)于GCD常用方法的介紹:
    http://www.reibang.com/p/3f9910293401
最后編輯于
?著作權(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)離奇詭異浅乔,居然都是意外死亡绢掰,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)童擎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)滴劲,“玉大人,你說(shuō)我怎么就攤上這事顾复“嗤冢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵芯砸,是天一觀(guān)的道長(zhǎng)萧芙。 經(jīng)常有香客問(wèn)我,道長(zhǎng)假丧,這世上最難降的妖魔是什么双揪? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮包帚,結(jié)果婚禮上渔期,老公的妹妹穿的比我還像新娘。我一直安慰自己渴邦,他們只是感情好疯趟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著谋梭,像睡著了一般信峻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瓮床,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天盹舞,我揣著相機(jī)與錄音,去河邊找鬼隘庄。 笑死踢步,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的峭沦。 我是一名探鬼主播贾虽,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼吼鱼!你這毒婦竟也來(lái)了蓬豁?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤菇肃,失蹤者是張志新(化名)和其女友劉穎地粪,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體琐谤,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蟆技,尸身上長(zhǎng)有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
  • 文/蒙蒙 一肛真、第九天 我趴在偏房一處隱蔽的房頂上張望谐丢。 院中可真熱鬧,春花似錦蚓让、人聲如沸庇谆。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)饭耳。三九已至,卻和暖如春执解,著一層夾襖步出監(jiān)牢的瞬間寞肖,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工衰腌, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留新蟆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓右蕊,卻偏偏與公主長(zhǎng)得像琼稻,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子饶囚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353