iOS開發(fā)中的鎖
本人對鎖沒有深入理解,只是看了幾篇文章混槐,在這里做一下簡單的總結(jié)戈锻。
iOS開發(fā)中歼跟,鎖是用來解決線程安全的問題的工具。那么線程安全是什么格遭?
線程安全
線程安全:多線程操作共享數(shù)據(jù)的時候哈街,如果出現(xiàn)了意想不到的結(jié)果,就是線程不安全拒迅。反之就是線程安全骚秦;
或者這么說是不是更容易聽懂,多個線程同時對一個數(shù)據(jù)進行修改的時候璧微,如果不能保證多個線程的執(zhí)行順序作箍,就會出現(xiàn)意想不到的結(jié)果,這個時候就線程不安全了前硫。
貌似怎么說都不行了蒙揣,那么就舉個例子吧;
- (void)threadNotSafe {
__block NSInteger total = 0;
for (NSInteger index = 0; index < 3; index++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
total += 1;
NSLog(@"total: %ld", total);
total -= 1;
NSLog(@"total: %ld", total);
});
}
}
/*
第一次打印結(jié)果
2019-05-06 11:04:33.937462+0800 DCLockStudy[5270:410073] total: 1
2019-05-06 11:04:33.937462+0800 DCLockStudy[5270:410074] total: 2
2019-05-06 11:04:33.937466+0800 DCLockStudy[5270:410075] total: 3
2019-05-06 11:04:33.937617+0800 DCLockStudy[5270:410075] total: 2
2019-05-06 11:04:33.937617+0800 DCLockStudy[5270:410073] total: 1
2019-05-06 11:04:33.937617+0800 DCLockStudy[5270:410074] total: 2
第二次打印結(jié)果
2019-05-06 11:06:50.198993+0800 DCLockStudy[5320:416449] total: 1
2019-05-06 11:06:50.198994+0800 DCLockStudy[5320:416450] total: 2
2019-05-06 11:06:50.199020+0800 DCLockStudy[5320:416452] total: 3
2019-05-06 11:06:50.199187+0800 DCLockStudy[5320:416450] total: 2
2019-05-06 11:06:50.199187+0800 DCLockStudy[5320:416449] total: 1
2019-05-06 11:06:50.199253+0800 DCLockStudy[5320:416452] total: 0
*/
上面這段代碼开瞭,分別執(zhí)行兩次懒震,打印結(jié)果不一樣。也就是不能確定代碼執(zhí)行順序和執(zhí)行結(jié)果嗤详,是線程不安全的个扰;
再看下面這段代碼
- (void)threadSafe {
__block NSInteger total = 0;
NSLock *myLock = [[NSLock alloc]init];
for (NSInteger index = 0; index < 3; index++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[myLock lock];
total += 1;
NSLog(@"total: %ld", total);
total -= 1;
NSLog(@"total: %ld", total);
[myLock unlock];
});
}
}
/*
第一次打印結(jié)果
2019-05-06 11:10:03.678707+0800 DCLockStudy[5351:422830] total: 1
2019-05-06 11:10:03.678872+0800 DCLockStudy[5351:422830] total: 0
2019-05-06 11:10:03.678978+0800 DCLockStudy[5351:422829] total: 1
2019-05-06 11:10:03.679057+0800 DCLockStudy[5351:422829] total: 0
2019-05-06 11:10:03.679189+0800 DCLockStudy[5351:422828] total: 1
2019-05-06 11:10:03.679286+0800 DCLockStudy[5351:422828] total: 0
第二次打印結(jié)果
2019-05-06 11:14:52.524955+0800 DCLockStudy[5406:431979] total: 1
2019-05-06 11:14:52.525092+0800 DCLockStudy[5406:431979] total: 0
2019-05-06 11:14:52.525224+0800 DCLockStudy[5406:431980] total: 1
2019-05-06 11:14:52.525303+0800 DCLockStudy[5406:431980] total: 0
2019-05-06 11:14:52.525413+0800 DCLockStudy[5406:431978] total: 1
2019-05-06 11:14:52.525511+0800 DCLockStudy[5406:431978] total: 0
*/
兩次打印結(jié)果一樣,為什么呢葱色?因為加了鎖递宅,哈哈哈;那么接下來我們來簡單說一下鎖;
鎖的幾個的定義
臨界區(qū):每個進程中訪問臨界資源的那段程序稱為臨界區(qū)办龄,每次只允許一個進程進入臨界區(qū)烘绽,進入后不允許其他進程進入。
互斥鎖:用于保護臨界區(qū)俐填,確保同一時間只有一個線程訪問數(shù)據(jù)安接。對共享資源的訪問,先對互斥量進行加鎖英融,如果互斥量已經(jīng)上鎖盏檐,調(diào)用線程會阻塞,直到互斥量被解鎖驶悟。在完成了對共享資源的訪問后胡野,要對互斥量進行解鎖。
接下來主要講幾種鎖:自旋鎖OSSpinLock痕鳍、信號量硫豆、pthread_mutex、NSLock笼呆、NSCondition熊响、NSRecursiveLock、NSConditionLock抄邀、@synchronized耘眨。這里參考了深入理解iOS開發(fā)中的鎖昼榛;
然后這些鎖我在學(xué)習(xí)過程中寫了一個簡單的demo境肾,里面有他們的使用方法;
自旋鎖OSSpinLock
自旋鎖與互斥鎖類似胆屿,它不是通過休眠使進程阻塞奥喻,而是在獲取鎖之前一直處于忙等(自旋)阻塞狀態(tài)。一般用于鎖持有的時間短非迹,而且線程并不希望在重新調(diào)度上花太多的成本环鲤。
自旋鎖與互斥鎖的區(qū)別:線程在申請自旋鎖的時候,線程不會被掛起憎兽,而是處于忙等的狀態(tài)冷离。
我所知道的自旋鎖只有OSSpinLock,不過YY大神已經(jīng)說過OSSpinLock不再安全了纯命,因此這里不做過多的介紹西剥,如果有興趣可以去看不再安全的 OSSpinLock;
信號量
dispatch_semaphore的實現(xiàn)原理和自旋鎖不一樣,是根據(jù)信號量判斷的亿汞,首先會將信號量-1瞭空,并判斷是否大于等于0,如果是,則返回0咆畏,并繼續(xù)執(zhí)行后續(xù)代碼南捂,否則,使線程進入睡眠狀態(tài)旧找,讓出cpu時間溺健。直到信號量大于0或者超時,則線程會被重新喚醒執(zhí)行后續(xù)操作钦讳。
使用方法如下
- (void)__dispatch_semaphore{
/**
dispatch_semaphore_create(1): 傳入值必須 >=0, 若傳入為 0 則阻塞線程并等待timeout,時間到后會執(zhí)行其后的語句
dispatch_semaphore_wait(signal, overTime):可以理解為 lock,會使得 signal 值 -1
dispatch_semaphore_signal(signal):可以理解為 unlock,會使得 signal 值 +1
停車場剩余4個車位矿瘦,那么即使同時來了四輛車也能停的下。如果此時來了五輛車愿卒,那么就有一輛需要等待缚去。
信號量的值(signal)就相當(dāng)于剩余車位的數(shù)目,dispatch_semaphore_wait 函數(shù)就相當(dāng)于來了一輛車琼开,dispatch_semaphore_signal 就相當(dāng)于走了一輛車易结。停車位的剩余數(shù)目在初始化的時候就已經(jīng)指明了(dispatch_semaphore_create(long value)),調(diào)用一次 dispatch_semaphore_signal柜候,剩余的車位就增加一個搞动;調(diào)用一次dispatch_semaphore_wait 剩余車位就減少一個;當(dāng)剩余車位為 0 時渣刷,再來車(即調(diào)用 dispatch_semaphore_wait)就只能等待鹦肿。有可能同時有幾輛車等待一個停車位。有些車主沒有耐心辅柴,給自己設(shè)定了一段等待時間箩溃,這段時間內(nèi)等不到停車位就走了,如果等到了就開進去停車碌嘀。而有些車主就像把車停在這涣旨,所以就一直等下去。
*/
dispatch_semaphore_t signal = dispatch_semaphore_create(1);//傳入值必須>=0;如果傳入0股冗,
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW,2.0f * NSEC_PER_SEC);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"線程1等待中霹陡。。止状。");
dispatch_semaphore_wait(signal, overTime);
NSLog(@"線程1");
sleep(1);
dispatch_semaphore_signal(signal);
NSLog(@"線程1發(fā)送信號");
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"線程2等待中烹棉。。怯疤。");
dispatch_semaphore_wait(signal, overTime);
NSLog(@"線程2");
sleep(1);
dispatch_semaphore_signal(signal);
NSLog(@"線程2發(fā)送信號");
});
}
pthread_mutex
pthread_mutex 表示互斥鎖浆洗。互斥鎖的實現(xiàn)原理與信號量非常相似旅薄,不是使用忙等辅髓,而是阻塞線程并睡眠泣崩,需要進行上下文切換。
使用方法如下:
- (void)__pthread_mutex_t{
/**
聲明 pthread_mutex_t pMutex;
創(chuàng)建一個互斥鎖pthread_mutex_init(&pMutex,PTHREAD_MUTEX_NORMAL);
PTHREAD_MUTEX_NORMAL 缺省類型洛口,也就是普通鎖矫付。當(dāng)一個線程加鎖以后,其余請求鎖的線程將形成一個等待隊列第焰,并在解鎖后先進先出原則獲得鎖买优。
PTHREAD_MUTEX_ERRORCHECK 檢錯鎖,如果同一個線程請求同一個鎖挺举,則返回 EDEADLK杀赢,否則與普通鎖類型動作相同。這樣就保證當(dāng)不允許多次加鎖時不會出現(xiàn)嵌套情況下的死鎖湘纵。
PTHREAD_MUTEX_RECURSIVE 遞歸鎖脂崔,允許同一個線程對同一個鎖成功獲得多次,并通過多次 unlock 解鎖梧喷。
PTHREAD_MUTEX_DEFAULT 適應(yīng)鎖砌左,動作最簡單的鎖類型,僅等待解鎖后重新競爭铺敌,沒有等待隊列汇歹。
加鎖 pthread_mutex_lock(&pMutex);
解鎖 pthread_mutex_unlock(&pMutex);
*/
//線程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"線程1加鎖");
pthread_mutex_lock(&pMutex);
sleep(1);
NSLog(@"線程1");
pthread_mutex_unlock(&pMutex);
NSLog(@"線程1解鎖");
});
//線程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1); //保證線程1先加鎖
NSLog(@"線程2加鎖");
pthread_mutex_lock(&pMutex);
NSLog(@"線程2");
pthread_mutex_unlock(&pMutex);
NSLog(@"線程2解鎖");
});
}
pthread_mutex 還支持遞歸鎖,只要將類型設(shè)置為PTHREAD_MUTEX_RECURSIVE
就可以偿凭。
NSLock
NSLock是OC以對象的形式暴露給開發(fā)者的一種鎖产弹,其實NSLock只是在內(nèi)部封裝了一個pthread_mutex
,屬性為PTHREAD_MUTEX_ERRORCHECK
,它會損失一定性能換來錯誤提示。
使用方法如下:
NSLock *lock = [NSLock new];
[lock lock];
//需要執(zhí)行的代碼
[lock unlock];
NSLock遵守了NSLocking
協(xié)議弯囊,NSLocking
協(xié)議其實很簡單痰哨,只需要滿足兩個方法
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
在這個基礎(chǔ)上,NSLock還自己提供兩個方法
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
tryLock
嘗試獲取鎖常挚,如果這個時候作谭,別的線程添加了鎖稽物,則返回NO
奄毡,不會阻塞線程;lockBeforeDate
嘗試在某個時間之前獲取鎖,如果在這個時間內(nèi)沒有獲取到鎖則返回NO
贝或,不會阻塞線程;
NSCondition
NSCondition其實是通過封裝了一個互斥鎖和條件變量吼过,把互斥鎖的lock方法和條件變量的wait/signal統(tǒng)一在NSCondition對象中,暴露給使用者咪奖。
NSCondition的加鎖過程和NSLock幾乎一致盗忱,耗時上應(yīng)該差不多。
使用方法如下:
- (void)__NSCondition{
/** 條件變量
wait:進入等待狀態(tài)
waitUntilDate::讓一個線程等待一定的時間
signal:喚醒一個等待的線程
broadcast:喚醒所有等待的線程
*/
//線程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.condition lock];
NSLog(@"線程1獲取到鎖羊赵,并進入等待狀態(tài)");
[self.condition wait];
NSLog(@"線程1等待完成");
[self.condition unlock];
NSLog(@"線程1解鎖");
});
//線程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.condition lock];
NSLog(@"線程2獲取到鎖趟佃,并進入等待狀態(tài)");
[self.condition wait];
NSLog(@"線程2等待完成");
[self.condition unlock];
NSLog(@"線程2解鎖");
});
//線程3
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
NSLog(@"線程3扇谣,喚醒一個等待線程");
[self.condition signal];
sleep(2);
NSLog(@"線程3,喚醒所有等待線程");
[self.condition broadcast];
});
}
NSConditionLock
NSConditionLock
借助 NSCondition
來實現(xiàn)闲昭,它的本質(zhì)就是一個生產(chǎn)者-消費者模型罐寨。“條件被滿足”可以理解為生產(chǎn)者提供了新的內(nèi)容序矩。NSConditionLock
的內(nèi)部持有一個 NSCondition
對象鸯绿,以及 _condition_value
屬性,在初始化時就會對這個屬性進行賦值簸淀。
使用方法如下:
- (void)__NSConditionLock{
/**條件鎖 NSConditionLock
因為遵守了NSLocking協(xié)議瓶蝴,所以可以無條件加鎖lock,
*/
dispatch_queue_t conditionLockQueue = dispatch_queue_create("conditionLockQueue", DISPATCH_QUEUE_CONCURRENT);
//線程1
dispatch_async(conditionLockQueue, ^{
NSLog(@"進入線程1租幕,添加條件鎖 = 2舷手,如果condition != 2,則線程阻塞");
[self.conditionLock lockWhenCondition:2];
NSLog(@"線程1加鎖成功,lockWhenCondition:2");
[self.conditionLock unlock];
NSLog(@"線程1解鎖鎖成功劲绪,unlockWithCondition:1");
});
//線程2
dispatch_async(conditionLockQueue, ^{
sleep(1); //保證線程3先執(zhí)行
NSLog(@"進入線程2聚霜,嘗試添加條件鎖 = 1");
if([self.conditionLock tryLockWhenCondition:1]){
NSLog(@"線程2加鎖成功,lockWhenCondition:1");
[self.conditionLock unlockWithCondition:2];
NSLog(@"線程2解鎖成功珠叔,unlockWithCondition:1");
}else{
NSLog(@"線程2加鎖失敗");
}
});
//線程3
dispatch_async(conditionLockQueue, ^{
// sleep(1); //保證線程2先執(zhí)行
NSLog(@"進入線程3蝎宇,嘗試添加條件鎖 = 0");
if([self.conditionLock tryLockWhenCondition:0]){
NSLog(@"線程3加鎖成功,lockWhenCondition:0");
[self.conditionLock unlockWithCondition:1];
NSLog(@"線程3解鎖成功祷安,unlockWithCondition:1");
}else{
NSLog(@"線程3加鎖失敗");
}
});
/** 先進入線程1姥芥,條件不滿足,線程1阻塞汇鞭,
進入線程3凉唐,線程3滿足條件,線程3加鎖霍骄,線程3解鎖并將條件設(shè)置為1台囱;
進入線程2,線程滿足條件读整,線程2加鎖簿训,線程2解鎖,并將條件設(shè)置為2米间;
因為條件為2强品,線程1滿足條件了,線程1不在阻塞屈糊,線程1加鎖的榛,線程1解鎖;
*/
}
NSRecursiveLock
遞歸鎖是通過pthread_mutex_lock
函數(shù)來實現(xiàn)的逻锐。
NSRecursiveLock
與 NSLock
的區(qū)別在于內(nèi)部封裝的 pthread_mutex_t
對象的類型不同夫晌,前者的類型為 PTHREAD_MUTEX_RECURSIVE
雕薪。
使用方法如下:
- (void)__NSRecursiveLock{
/** NSRecursiveLock遞歸鎖
它可以被同一線程多次請求,但不會引起死鎖晓淀。這主要是用在循環(huán)或者遞歸操作場景中蹦哼。
*/
/** 如果用普通鎖,當(dāng)?shù)诙芜M入遞歸方法時要糊,嘗試加鎖纲熏,但是這個時候該線程還處于鎖定,所以線程阻塞锄俄,相互等待造成死鎖局劲。
所以這個時候可以用一個遞歸鎖,允許同一線程多次請求奶赠,并且不會死鎖鱼填;
NSRecursiveLock 遞歸鎖和NSLock一樣也有tryLock和lockBeforeDate,用法一樣毅戈;
*/
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^ recursiveMethod)(NSInteger);
recursiveMethod = ^(NSInteger num){
// [self.myLock lock];
[self.recursiveLock lock];
if(num > 0){
num --;
NSLog(@"start num = %ld, mutableArray[0] = %@",num,self.mutableArray[0]);
sleep(1);
[self.mutableArray replaceObjectAtIndex:0 withObject:[NSString stringWithFormat:@"replace = %ld",num]];
NSLog(@"end num = %ld, mutableArray[0] = %@",num,self.mutableArray[0]);
recursiveMethod(num);
}
// [self.myLock unlock];
[self.recursiveLock unlock];
};
recursiveMethod(5);
});
}
@synchronized
這其實是一個OC層面的鎖苹丸,主要通過犧牲新能來換取語法上的簡潔與可讀。
我們知道 @synchronized 后面需要緊跟一個 OC 對象苇经,它實際上是把這個對象當(dāng)做鎖來使用赘理。這是通過一個哈希表來實現(xiàn)的,OC 在底層使用了一個互斥鎖的數(shù)組(你可以理解為鎖池)扇单,通過對對象去哈希值來得到對應(yīng)的互斥鎖商模。
具體實現(xiàn)原理可以參考 關(guān)于 @synchronized,這兒比你想知道的還要多蜘澜。
[demo]
這是我自己學(xué)習(xí)的時候?qū)懙囊粋€demo施流,是各種鎖的使用。上面的代碼基本都是這個demo的代碼鄙信;
參考資料
1瞪醋、深入理解iOS開發(fā)中的鎖