demo下載
建議一邊看文章,一邊看代碼。
聲明:關(guān)于性能的分析是基于我的測試代碼來的,我也看到和網(wǎng)上很多測試結(jié)果有所不同,所以性能分析只作為參考,僅代表測試代碼表現(xiàn),不代表真實情況。同時我會基于我的代碼盡量讓性能測試更精準(zhǔn)。
線程安全是怎么產(chǎn)生的
常見比如線程內(nèi)操作了一個線程外的非線程安全變量
,這個時候一定要考慮線程安全和同步。
- (void)removeLastIamgeName{//假如每個進(jìn)來的都是不同的線程
//self.imageNames是NSMutableArray
if (self.imageNames.count>0) {
//比如當(dāng)前count為1,那么第一個線程和第二個線程都可以進(jìn)入判斷內(nèi)部,第一個線程刪除了數(shù)組里面最后一個數(shù)據(jù),第二個線程去刪除的時候因為已經(jīng)沒有數(shù)據(jù)了,count=0,這個時候取調(diào)用removeObjectAtIndex:0機(jī)會crash,數(shù)組越界了
[self.imageNames removeObjectAtIndex:self.imageNames.count-1];
}
}
下面是鎖的同步方案
鎖的概念
鎖是最常用的同步工具。一段代碼段在同一個時間只能允許被一個線程訪問,比如一個線程A進(jìn)入加鎖代碼之后由于已經(jīng)加鎖,另一個線程B就無法訪問,只有等待前一個線程A執(zhí)行完加鎖代碼后解鎖,B線程才能訪問加鎖代碼。
不要將過多的其他操作代碼放到里面,否則一個線程執(zhí)行的時候另一個線程就一直在等待,就無法發(fā)揮多線程的作用了。
NSLock
在Cocoa程序中NSLock中實現(xiàn)了一個簡單的互斥鎖,實現(xiàn)了NSLocking protocol。
lock,加鎖
unlock,解鎖
tryLock,嘗試加鎖,如果失敗了,并不會阻塞線程,只是立即返回,如果可以加鎖,會進(jìn)行加鎖
NOlockBeforeDate:,在指定的date之前暫時阻塞線程(如果沒有獲取鎖的話),如果到期還沒有獲取鎖,則線程被喚醒,函數(shù)立即返回NO
使用tryLock并不能成功加鎖,如果獲取鎖失敗就不會執(zhí)行加鎖代碼了。
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSString *imageName;
[lock lock];
if (imageNames.count>0) {
imageName = [imageNames lastObject];
[imageNames removeObject:imageName];
}
[lock unlock];
}
@synchronized代碼塊
每個iOS開發(fā)最早接觸的線程鎖就是@synchronized,代碼簡單。
- (void)getIamgeName:(int)index{
NSString *imageName;
@synchronized(self) {
if (imageNames.count>0) {
imageName = [imageNames lastObject];
[imageNames removeObject:imageName];
}
}
}
條件信號量dispatch_semaphore_t
dispatch_semaphore_t
GCD中信號量,也可以解決資源搶占問題,支持信號通知和信號等待。每當(dāng)發(fā)送一個信號通知,則信號量+1;每當(dāng)發(fā)送一個等待信號時信號量-1,;如果信號量為0則信號會處于等待狀態(tài),直到信號量大于0開始執(zhí)行。
#import "MYDispatchSemaphoreViewController.h"
@interface MYDispatchSemaphoreViewController ()
{
dispatch_semaphore_t semaphore;
}
@end
@implementation MYDispatchSemaphoreViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
semaphore = dispatch_semaphore_create(1);
/**
* 創(chuàng)建一個信號量為1的信號
*
*/
}
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSString *imageName;
/**
* semaphore:等待信號
DISPATCH_TIME_FOREVER:等待時間
wait之后信號量-1,為0
*/
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (imageNames.count>0) {
imageName = [imageNames lastObject];
[imageNames removeObject:imageName];
}
/**
* 發(fā)送一個信號通知,這時候信號量+1,為1
*/
dispatch_semaphore_signal(semaphore);
}
@end
條件鎖NSCondition
NSCondition同樣實現(xiàn)了NSLocking協(xié)議,所以它和NSLock一樣,也有NSLocking協(xié)議的lock和unlock方法,可以當(dāng)做NSLock來使用解決線程同步問題,用法完全一樣。
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSString *imageName;
[lock lock];
if (imageNames.count>0) {
imageName = [imageNames lastObject];
[imageNames removeObject:imageName];
}
[lock unlock];
}
同時,NSCondition提供更高級的用法。wait和signal,和條件信號量類似。
比如我們要監(jiān)聽imageNames數(shù)組的個數(shù),當(dāng)imageNames的個數(shù)大于0的時候就執(zhí)行清空操作。思路是這樣的,當(dāng)imageNames個數(shù)大于0時執(zhí)行清空操作,否則,wait等待執(zhí)行清空操作。當(dāng)imageNames個數(shù)增加的時候發(fā)生signal信號,讓等待的線程喚醒繼續(xù)執(zhí)行。
NSCondition和NSLock、@synchronized等是不同的是,NSCondition可以給每個線程分別加鎖,加鎖后不影響其他線程進(jìn)入臨界區(qū)。這是非常強(qiáng)大。
但是正是因為這種分別加鎖的方式,NSCondition使用wait并使用加鎖后并不能真正的解決資源的競爭。比如我們有個需求:不能讓m<0。假設(shè)當(dāng)前m=0,線程A要判斷到m>0為假,執(zhí)行等待;線程B執(zhí)行了m=1操作,并喚醒線程A執(zhí)行m-1操作的同時線程C判斷到m>0,因為他們在不同的線程鎖里面,同樣判斷為真也執(zhí)行了m-1,這個時候線程A和線程C都會執(zhí)行m-1,但是m=1,結(jié)果就會造成m=-1.
當(dāng)我用數(shù)組做刪除試驗時,做增刪操作并不是每次都會出現(xiàn),大概3-4次后會出現(xiàn)。單純的使用lock、unlock是沒有問題的。
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSString *imageName;
[lock lock]; //加鎖
static int m = 0;
static int n = 0;
static int p = 0;
NSLog(@"removeObjectBegin count: %ld\n",imageNames.count);
if (imageNames.count>0) {
imageName = [imageNames lastObject];
[imageNames removeObjectAtIndex:0];
m++;
NSLog(@"執(zhí)行了%d次刪除操作",m);
} else {
p++;
NSLog(@"執(zhí)行了%d次等待",p);
[lock wait]; //等待
imageName = [imageNames lastObject];
[imageNames removeObjectAtIndex:0];
/**
* 有時候點擊取出圖片會崩潰
*/
n++;
NSLog(@"執(zhí)行了%d次繼續(xù)操作",n);
}
NSLog(@"removeObject count: %ld\n",imageNames.count);
[lock unlock]; //解鎖
}
- (void)createImageName:(NSMutableArray *)imageNames{
[lock lock];
static int m = 0;
[imageNames addObject:@"0"];
m++;
NSLog(@"添加了%d次",m);
[lock signal]; //喚醒隨機(jī)一個線程取消等待繼續(xù)執(zhí)行
// [lock broadcast]; //喚醒所有線程取消等待繼續(xù)執(zhí)行
NSLog(@"createImageName count: %ld\n",imageNames.count);
[lock unlock];
}
#pragma mark - 多線程取出圖片后刪除
- (void)getImageNameWithMultiThread{
[lock broadcast];
NSMutableArray *imageNames = [[NSMutableArray alloc]init];
dispatch_group_t dispatchGroup = dispatch_group_create();
__block double then, now;
then = CFAbsoluteTimeGetCurrent();
for (int i=0; i<10; i++) {
dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
[self getIamgeName:imageNames];
});
dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
[self createImageName:imageNames];
});
}
dispatch_group_notify(dispatchGroup, self.synchronizationQueue, ^(){
now = CFAbsoluteTimeGetCurrent();
printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
});
}
條件鎖NSConditionLock
也有人說這是個互斥鎖
NSConditionLock同樣實現(xiàn)了NSLocking協(xié)議,試驗過程中發(fā)現(xiàn)性能很低。
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSString *imageName;
[lock lock];
if (imageNames.count>0) {
imageName = [imageNames lastObject];
[imageNames removeObject:imageName];
}
[lock unlock];
}
NSConditionLock也可以像NSCondition一樣做多線程之間的任務(wù)等待調(diào)用,而且是線程安全的。
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSString *imageName;
[lock lockWhenCondition:1]; //加鎖
if (imageNames.count>0) {
imageName = [imageNames lastObject];
[imageNames removeObjectAtIndex:0];
}
[lock unlockWithCondition:0]; //解鎖
}
- (void)createImageName:(NSMutableArray *)imageNames{
[lock lockWhenCondition:0];
[imageNames addObject:@"0"];
[lock unlockWithCondition:1];
}
#pragma mark - 多線程取出圖片后刪除
- (void)getImageNameWithMultiThread{
NSMutableArray *imageNames = [[NSMutableArray alloc]init];
dispatch_group_t dispatchGroup = dispatch_group_create();
__block double then, now;
then = CFAbsoluteTimeGetCurrent();
for (int i=0; i<10000; i++) {
dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
[self getIamgeName:imageNames];
});
dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
[self createImageName:imageNames];
});
}
dispatch_group_notify(dispatchGroup, self.synchronizationQueue, ^(){
now = CFAbsoluteTimeGetCurrent();
printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
});
}
遞歸鎖NSRecursiveLock
有時候“加鎖代碼”中存在遞歸調(diào)用,遞歸開始前加鎖,遞歸調(diào)用開始后會重復(fù)執(zhí)行此方法以至于反復(fù)執(zhí)行加鎖代碼最終造成死鎖,這個時候可以使用遞歸鎖來解決。使用遞歸鎖可以在一個線程中反復(fù)獲取鎖而不造成死鎖,這個過程中會記錄獲取鎖和釋放鎖的次數(shù),只有最后兩者平衡鎖才被最終釋放。
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSString *imageName;
[lock lock];
if (imageNames.count>0) {
imageName = [imageNames firstObject];
[imageNames removeObjectAtIndex:0];
[self getIamgeName:imageNames];
}
[lock unlock];
}
- (void)getImageNameWithMultiThread{
NSMutableArray *imageNames = [NSMutableArray new];
int count = 1024*10;
for (int i=0; i<count; i++) {
[imageNames addObject:[NSString stringWithFormat:@"%d",i]];
}
dispatch_group_t dispatchGroup = dispatch_group_create();
__block double then, now;
then = CFAbsoluteTimeGetCurrent();
dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
[self getIamgeName:imageNames];
});
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){
now = CFAbsoluteTimeGetCurrent();
printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
});
}
NSDistributedLock
NSDistributedLock是MAC開發(fā)中的跨進(jìn)程的分布式鎖,底層是用文件系統(tǒng)實現(xiàn)的互斥鎖。NSDistributedLock沒有實現(xiàn)NSLocking協(xié)議,所以沒有l(wèi)ock方法,取而代之的是非阻塞的tryLock方法。
NSDistributedLock *lock = [[NSDistributedLock alloc] initWithPath:@"/Users/mac/Desktop/lock.lock"];
while (![lock tryLock])
{
sleep(1);
}
//do something
[lock unlock];
當(dāng)執(zhí)行到do something時程序退出,程序再次啟動之后tryLock就再也不能成功了,陷入死鎖狀態(tài).其他應(yīng)用也不能訪問受保護(hù)的共享資源。在這種情況下,你可以使用breadLock方法來打破現(xiàn)存的鎖以便你可以獲取它。但是通常應(yīng)該避免打破鎖,除非你確定擁有進(jìn)程已經(jīng)死亡并不可能再釋放該鎖。
因為是MAC下的線程鎖,所以demo里面沒有,這里也不做過多關(guān)注。
互斥鎖POSIX
POSIX和dispatch_semaphore_t很像,但是完全不同。POSIX是Unix/Linux平臺上提供的一套條件互斥鎖的API。
新建一個簡單的POSIX互斥鎖,引入頭文件#import <pthread.h>
聲明并初始化一個pthread_mutex_t的結(jié)構(gòu)。使用pthread_mutex_lock和pthread_mutex_unlock函數(shù)。調(diào)用pthread_mutex_destroy來釋放該鎖的數(shù)據(jù)結(jié)構(gòu)。
#import <pthread.h>
@interface MYPOSIXViewController ()
{
pthread_mutex_t mutex; //聲明pthread_mutex_t的結(jié)構(gòu)
}
@end
@implementation MYPOSIXViewController
- (void)dealloc{
pthread_mutex_destroy(&mutex); //釋放該鎖的數(shù)據(jù)結(jié)構(gòu)
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
pthread_mutex_init(&mutex, NULL);
/**
* 初始化
*
*/
}
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSString *imageName;
/**
* 加鎖
*/
pthread_mutex_lock(&mutex);
if (imageNames.count>0) {
imageName = [imageNames firstObject];
[imageNames removeObjectAtIndex:0];
}
/**
* 解鎖
*/
pthread_mutex_unlock(&mutex);
}
POSIX還可以創(chuàng)建條件鎖,提供了和NSCondition一樣的條件控制,初始化互斥鎖同時使用pthread_cond_init來初始化條件數(shù)據(jù)結(jié)構(gòu),
// 初始化
int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
// 等待(會阻塞)
int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
// 定時等待
int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);
// 喚醒
int pthread_cond_signal (pthread_cond_t *cond);
// 廣播喚醒
int pthread_cond_broadcast (pthread_cond_t *cond);
// 銷毀
int pthread_cond_destroy (pthread_cond_t *cond);
POSIX還提供了很多函數(shù),有一套完整的API,包含Pthreads線程的創(chuàng)建控制等等,非常底層,可以手動處理線程的各個狀態(tài)的轉(zhuǎn)換即管理生命周期,甚至可以實現(xiàn)一套自己的多線程,感興趣的可以繼續(xù)深入了解。推薦一篇詳細(xì)文章,但不是基于iOS的,是基于Linux的,但是介紹的非常詳細(xì) Linux 線程鎖詳解
自旋鎖OSSpinLock
首先要提的是OSSpinLock已經(jīng)出現(xiàn)了BUG,導(dǎo)致并不能完全保證是線程安全的。
新版 iOS 中,系統(tǒng)維護(hù)了 5 個不同的線程優(yōu)先級/QoS: background,utility,default,user-initiated,user-interactive。高優(yōu)先級線程始終會在低優(yōu)先級線程前執(zhí)行,一個線程不會受到比它更低優(yōu)先級線程的干擾。這種線程調(diào)度算法會產(chǎn)生潛在的優(yōu)先級反轉(zhuǎn)問題,從而破壞了 spin lock。
具體來說,如果一個低優(yōu)先級的線程獲得鎖并訪問共享資源,這時一個高優(yōu)先級的線程也嘗試獲得這個鎖,它會處于 spin lock 的忙等狀態(tài)從而占用大量 CPU。此時低優(yōu)先級線程無法與高優(yōu)先級線程爭奪 CPU 時間,從而導(dǎo)致任務(wù)遲遲完不成、無法釋放 lock。這并不只是理論上的問題,libobjc 已經(jīng)遇到了很多次這個問題了,于是蘋果的工程師停用了 OSSpinLock。
蘋果工程師 Greg Parker 提到,對于這個問題,一種解決方案是用 truly unbounded backoff 算法,這能避免 livelock 問題,但如果系統(tǒng)負(fù)載高時,它仍有可能將高優(yōu)先級的線程阻塞數(shù)十秒之久;另一種方案是使用 handoff lock 算法,這也是 libobjc 目前正在使用的。鎖的持有者會把線程 ID 保存到鎖內(nèi)部,鎖的等待者會臨時貢獻(xiàn)出它的優(yōu)先級來避免優(yōu)先級反轉(zhuǎn)的問題。理論上這種模式會在比較復(fù)雜的多鎖條件下產(chǎn)生問題,但實踐上目前還一切都好。
OSSpinLock 自旋鎖,性能最高的鎖。原理很簡單,就是一直 do while 忙等。它的缺點是當(dāng)?shù)却龝r會消耗大量 CPU 資源,所以它不適用于較長時間的任務(wù)。對于內(nèi)存緩存的存取來說,它非常合適。
-摘自ibireme
所以說不建議再繼續(xù)使用,不過可以拿來玩耍一下,導(dǎo)入頭文件#import <libkern/OSAtomic.h>
#import <libkern/OSAtomic.h>
@interface MYOSSpinLockViewController ()
{
OSSpinLock spinlock; //聲明pthread_mutex_t的結(jié)構(gòu)
}
@end
@implementation MYOSSpinLockViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
spinlock = OS_SPINLOCK_INIT;
/**
* 初始化
*
*/
}
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSString *imageName;
/**
* 加鎖
*/
OSSpinLockLock(&spinlock);
if (imageNames.count>0) {
imageName = [imageNames firstObject];
[imageNames removeObjectAtIndex:0];
}
/**
* 解鎖
*/
OSSpinLockUnlock(&spinlock);
}
@end
OSSpinLock的性能真的很卓越,可惜啦
GCD線程阻斷dispatch_barrier_async/dispatch_barrier_sync
dispatch_barrier_async/dispatch_barrier_sync在一定的基礎(chǔ)上也可以做線程同步,會在線程隊列中打斷其他線程執(zhí)行當(dāng)前任務(wù),也就是說只有用在并發(fā)的線程隊列中才會有效,因為串行隊列本來就是一個一個的執(zhí)行的,你打斷執(zhí)行一個和插入一個是一樣的效果。兩個的區(qū)別是是否等待任務(wù)執(zhí)行完成。
注意:如果在當(dāng)前線程調(diào)用dispatch_barrier_sync打斷會發(fā)生死鎖。
@interface MYdispatch_barrier_syncViewController ()
{
__block double then, now;
}
@end
@implementation MYdispatch_barrier_syncViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSString *imageName;
if (imageNames.count>0) {
imageName = [imageNames firstObject];
[imageNames removeObjectAtIndex:0];
}else{
now = CFAbsoluteTimeGetCurrent();
printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
}
}
- (void)getImageNameWithMultiThread{
NSMutableArray *imageNames = [NSMutableArray new];
int count = 1024*11;
for (int i=0; i<count; i++) {
[imageNames addObject:[NSString stringWithFormat:@"%d",i]];
}
then = CFAbsoluteTimeGetCurrent();
for (int i=0; i<count+1; i++) {
//100來測試鎖有沒有正確的執(zhí)行
dispatch_barrier_async(self.synchronizationQueue, ^{
[self getIamgeName:imageNames];
});
}
}
總結(jié)
@synchronized:適用線程不多,任務(wù)量不大的多線程加鎖
NSLock:其實NSLock并沒有想象中的那么差,不知道大家為什么不推薦使用
dispatch_semaphore_t:使用信號來做加鎖,性能提升顯著
NSCondition:使用其做多線程之間的通信調(diào)用不是線程安全的
NSConditionLock:單純加鎖性能非常低,比NSLock低很多,但是可以用來做多線程處理不同任務(wù)的通信調(diào)用
NSRecursiveLock:遞歸鎖的性能出奇的高,但是只能作為遞歸使用,所以限制了使用場景
NSDistributedLock:因為是MAC開發(fā)的,就不討論了
POSIX(pthread_mutex):底層的api,復(fù)雜的多線程處理建議使用,并且可以封裝自己的多線程
OSSpinLock:性能也非常高,可惜出現(xiàn)了線程問題
dispatch_barrier_async/dispatch_barrier_sync:測試中發(fā)現(xiàn)dispatch_barrier_sync比dispatch_barrier_async性能要高,真是大出意外
下面是基準(zhǔn)測試
模擬器環(huán)境:i5 2.6GH+8G 內(nèi)存,xcode 7.2.1 (7C1002)+iPhone6SP(9.2)
真機(jī)環(huán)境:xcode 7.2.1 (7C1002)+iPhone6(國行)
通過測試發(fā)現(xiàn)模擬器和真機(jī)的區(qū)別還是很大的,模擬器上明顯的階梯感,真機(jī)就沒有,模擬器上NSConditionLock的性能非常差,我沒有把它的參數(shù)加在表格上,不然其他的就看不到了。不過真機(jī)上面性能還好。
這些性能測試只是一個參考,沒必要非要去在意這些,畢竟前端的編程一般線程要求沒那么高,可以從其他的地方優(yōu)化。線程安全中注意避坑,另外選擇自己喜歡的方式,這樣你可以研究的更深入,使用的更熟練。
另外,demo中我把邏輯拿了出來,算是一個小小的MVVM框架或者M(jìn)VVCC框架吧
demo在最上方。
2016.6.30更新
有網(wǎng)友提醒我有些鎖在資源競爭激烈和不激烈的情況下性能有差別,于是我修改了源碼,將原來的開辟大量線程邏輯改為開辟3個線程,代碼已更新github,老代碼在標(biāo)簽1.0的位置,有興趣可以看下。
根據(jù)新的測試結(jié)果,dispatch_barrier_async、dispatch_semaphore_t 、OSSpinLock,三種鎖在資源競爭程度不同下表現(xiàn)比較明顯。
2021.09.17更新
dispatch_barrier_sync : 0.001312
dispatch_barrier_async : 0.001331
OSSpinLock : 0.001828
os_unfair_lock : 0.003643
POSIX : 0.005202
NSLock : 0.005666
NSCondition : 0.005898
synchronized : 0.007310
NSConditionLock : 0.018997
DispatchSemaphore : 0.031588
另外再次聲明:測試結(jié)果僅僅代表一個參考,因為各種因素的影響,并沒有那么準(zhǔn)確。還是那句話,選擇自己喜歡的加鎖方式,高大上的還是性能高的,自己選擇,沒必要太在意對比。