在iOS編碼中,鎖的出現(xiàn)其實是因為多線程會出現(xiàn)線程安全的問題镊靴。那么铣卡,問題來了,什么是線程安全偏竟?為什么鎖可以解決線程安全問題煮落?單線程是不是絕對的線程安全?iOS編程有多少種鎖踊谋?加解鎖的效率如何蝉仇?......
一、什么是線程安全?
WIKI: Thread-safe code only manipulates shared data structures in a manner that ensures that all threads behave properly and fulfil their design specifications without unintended interaction.
用人話來說:多線程操作共享數(shù)據(jù)不會出現(xiàn)想不到的結果就是線程安全的轿衔,否則沉迹,是線程不安全的。
舉個例子:
NSInteger total = 0;
- (void)threadNotSafe {
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);
});
}
}
//第一次輸出:
2017-11-28 23:34:11.551570+0800 BasicDemo[75679:5312246] total: 1
2017-11-28 23:34:11.551619+0800 BasicDemo[75679:5312248] total: 3
2017-11-28 23:34:11.551618+0800 BasicDemo[75679:5312249] total: 2
2017-11-28 23:34:11.552120+0800 BasicDemo[75679:5312246] total: 2
2017-11-28 23:34:11.552143+0800 BasicDemo[75679:5312248] total: 1
2017-11-28 23:34:11.552171+0800 BasicDemo[75679:5312249] total: 0
//第二次輸出
2017-11-28 23:34:55.738947+0800 BasicDemo[75683:5313401] total: 1
2017-11-28 23:34:55.738979+0800 BasicDemo[75683:5313403] total: 2
2017-11-28 23:34:55.738985+0800 BasicDemo[75683:5313402] total: 3
2017-11-28 23:34:55.739565+0800 BasicDemo[75683:5313401] total: 2
2017-11-28 23:34:55.739570+0800 BasicDemo[75683:5313402] total: 1
2017-11-28 23:34:55.739577+0800 BasicDemo[75683:5313403] total: 0
NSInteger total = 0;
NSLock *lock = [NSLock new];
- (void)threadSafe {
for (NSInteger index = 0; index < 3; index++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
total += 1;
NSLog(@"total: %ld", total);
total -= 1;
NSLog(@"total: %ld", total);
[lock unlock];
});
}
}
//第一次輸出
2017-11-28 23:35:37.696614+0800 BasicDemo[75696:5314483] total: 1
2017-11-28 23:35:37.696928+0800 BasicDemo[75696:5314483] total: 0
2017-11-28 23:35:37.696971+0800 BasicDemo[75696:5314481] total: 1
2017-11-28 23:35:37.696995+0800 BasicDemo[75696:5314481] total: 0
2017-11-28 23:35:37.697026+0800 BasicDemo[75696:5314482] total: 1
2017-11-28 23:35:37.697050+0800 BasicDemo[75696:5314482] total: 0
//第二次輸出
2017-11-28 23:36:01.790264+0800 BasicDemo[75700:5315159] total: 1
2017-11-28 23:36:01.790617+0800 BasicDemo[75700:5315159] total: 0
2017-11-28 23:36:01.790668+0800 BasicDemo[75700:5315161] total: 1
2017-11-28 23:36:01.790687+0800 BasicDemo[75700:5315161] total: 0
2017-11-28 23:36:01.790711+0800 BasicDemo[75700:5315160] total: 1
2017-11-28 23:36:01.790735+0800 BasicDemo[75700:5315160] total: 0
第一個函數(shù)第一次和第二次調用的結果不一樣害驹,換句話說鞭呕,不能確定代碼的運行順序和結果,是線程不安全的宛官;第二個函數(shù)第一次和第二次輸出結果一樣葫松,可以確定函數(shù)的執(zhí)行結果,是線程安全的底洗。
居于線程安全的含義腋么,知道線程安全是相對于多線程而言的,單線程不會存在線程安全問題亥揖。因為珊擂,單線程代碼的執(zhí)行順序是確定的,可以知道代碼的執(zhí)行結果徐块。
二未玻、鎖鎖鎖??
線程不安全是由于多線程訪問造成的,那么如何解決胡控?
1.既然線程安全問題是由多線程引起的扳剿,那么,最極端的可以使用單線程保證線程安全昼激。
2.線程安全是由于多線程訪問和修改共享資源而引起不可預測的結果庇绽,因此,如果都是訪問共享資源而不去修改共享資源也可以保證線程安全橙困,比如:設置只讀屬性的全局變量瞧掺。
3.使用鎖。
引用 ibireme 在《不再安全的 OSSpinLock》中的一張圖片說明加解鎖的效率:
我也下載了 ibireme 在 GitHub 上面的Demo來跑過(環(huán)境 iPhone6 iOS11.1)凡傅。發(fā)現(xiàn)辟狈,不同的循環(huán)次數(shù),結果都不一樣夏跷,并沒有得到和 ibireme 一樣的結果哼转。所以,上面的柱狀圖也只做一個定向分析槽华,并不是很準確的結果壹蔓。
下面會對這些鎖的實現(xiàn)原理和用法做簡單的總結和處理(詳細的實現(xiàn)原理,可以看 bestswift 的這篇文章《深入理解 iOS 開發(fā)中的鎖》):
OSSpinLock:
自旋鎖的實現(xiàn)原理比較簡單猫态,就是死循環(huán)佣蓉。當a線程獲得鎖以后披摄,b線程想要獲取鎖就需要等待a線程釋放鎖。在沒有獲得鎖的期間勇凭,b線程會一直處于忙等的狀態(tài)疚膊。如果a線程在臨界區(qū)的執(zhí)行時間過長,則b線程會消耗大量的cpu時間套像,不太劃算酿联。所以,自旋鎖用在臨界區(qū)執(zhí)行時間比較短的環(huán)境性能會很高夺巩。
自旋鎖的代碼實現(xiàn):
#import <libkern/OSAtomic.h>
OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
//需要執(zhí)行的代碼
OSSpinLockUnlock(&lock);
//OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock)
//蘋果在OSSpinLock注釋表示被廢棄,改用不安全的鎖替代
dispatch_semaphore:
dispatch_semaphore實現(xiàn)的原理和自旋鎖有點不一樣周崭。首先會先將信號量減一柳譬,并判斷是否大于等于0,如果是续镇,則返回0美澳,并繼續(xù)執(zhí)行后續(xù)代碼,否則摸航,使線程進入睡眠狀態(tài)制跟,讓出cpu時間。直到信號量大于0或者超時酱虎,則線程會被重新喚醒執(zhí)行后續(xù)操作雨膨。
dispatch_semaphore_t lock = dispatch_semaphore_create(1); //傳入的參數(shù)必須大于或者等于0,否則會返回Null
long wait = dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); //wait = 0读串,則表示不需要等待聊记,直接執(zhí)行后續(xù)代碼;wait != 0恢暖,則表示需要等待信號或者超時排监,才能繼續(xù)執(zhí)行后續(xù)代碼。lock信號量減一杰捂,判斷是否大于0舆床,如果大于0則繼續(xù)執(zhí)行后續(xù)代碼;lock信號量減一少于或者等于0嫁佳,則等待信號量或者超時挨队。
//需要執(zhí)行的代碼
long signal = dispatch_semaphore_signal(lock); //signal = 0,則表示沒有線程需要其處理的信號量脱拼,換句話說瞒瘸,沒有需要喚醒的線程;signal != 0熄浓,則表示有一個或者多個線程需要喚醒情臭,則喚醒一個線程省撑。(如果線程有優(yōu)先級,則喚醒優(yōu)先級最高的線程俯在,否則竟秫,隨機喚醒一個線程。)
pthread_mutex:
pthread_mutex表示互斥鎖跷乐,和信號量的實現(xiàn)原理類似肥败,也是阻塞線程并進入睡眠,需要進行上下文切換愕提。
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
pthread_mutex_t lock;
pthread_mutex_init(&lock, &attr); //設置屬性
pthread_mutex_lock(&lock); //上鎖
//需要執(zhí)行的代碼
pthread_mutex_unlock(&lock); //解鎖
NSLock:
NSLock在內部封裝了一個 pthread_mutex馒稍,屬性為 PTHREAD_MUTEX_ERRORCHECK。
NSLock *lock = [NSLock new];
[lock lock];
//需要執(zhí)行的代碼
[lock unlock];
NSCondition:
NSCondition封裝了一個互斥鎖和條件變量浅侨∨耍互斥鎖保證線程安全,條件變量保證執(zhí)行順序如输。
NSCondition *lock = [NSCondition new];
[lock lock];
//需要執(zhí)行的代碼
[lock unlock];
pthread_mutex(recursive):
pthread_mutex鎖的一種鼓黔,屬于遞歸鎖。一般一個線程只能申請一把鎖不见,但是澳化,如果是遞歸鎖,則可以申請很多把鎖稳吮,只要上鎖和解鎖的操作數(shù)量就不會報錯缎谷。
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_t lock;
pthread_mutex_init(&lock, &attr); //設置屬性
pthread_mutex_lock(&lock); //上鎖
//需要執(zhí)行的代碼
pthread_mutex_unlock(&lock); //解鎖
NSRecursiveLock:
遞歸鎖,pthread_mutex(recursive)的封裝盖高。
NSRecursiveLock *lock = [NSRecursiveLock new];
[lock lock];
//需要執(zhí)行的代碼
[lock unlock];
NSConditionLock:
NSConditionLock借助 NSCondition 來實現(xiàn)慎陵,本質是生產者-消費者模型。
NSConditionLock *lock = [NSConditionLock new];
[lock lock];
//需要執(zhí)行的代碼
[lock unlock];
@synchronized:
一個對象層面的鎖喻奥,鎖住了整個對象席纽,底層使用了互斥遞歸鎖來實現(xiàn)。
NSObject *object = [NSObject new];
@synchronized(object) {
//需要執(zhí)行的代碼
}
三撞蚕、總結
這里只是一些簡單的總結润梯,更多深入的研究請自行 Google。
參考:
深入理解 iOS 開發(fā)中的鎖
不再安全的 OSSpinLock
Threading Programming Guide
關于 @synchronized甥厦,這兒比你想知道的還要多
OS中保證線程安全的幾種方式與性能對比
iOS 常見知識點(三):Lock
iOS多線程到底不安全在哪里纺铭?