什么會(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é)果
所以根據(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é)果就沒問題了賣票的問題,我們針對(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)處霸琴,可以加鎖成功
Debug
-> Debug Workflow
-> Always Show Disassembly
切換到匯編界面哮洽。我們可以在控制臺(tái)通過命令si
執(zhí)行一句匯編代碼填渠,入當(dāng)前匯編指令是調(diào)用函數(shù)的話,便會(huì)進(jìn)入到該函數(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)問題
自旋鎖的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)
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)看下圖
總結(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)系骚秦。再用下圖說明一下
上面介紹的這幾個(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
可以看到龙巨,線程會(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_enter
和objc_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
的氏豌。
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)悬垃,如下圖
可以看到,如果屬性被
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中多線程的同步問題和多線程讀寫安全問題的解決方案,就整理到這里奠旺。