前言
在iOS
平臺中袍祖,開發(fā)者都知道幾乎所有的屬性都應(yīng)該用nonatomic
修飾底瓣,那么為什么呢?相信不少初學(xué)者都應(yīng)該看到過stackoverflow上的一個問題:What's the difference between the atomic and nonatomic attributes?
其中一個回答中提到在非競爭并且一些極端的環(huán)境下atomic
修飾屬性的讀寫方法比nonatomic
慢20倍(但并沒有指明到底如何算極端)所以平時在自己項目工程里以及一些第三方庫中很少見到有用atomic
修飾屬性的盲泛,所以我們定義屬性的時候一般想都沒想第一個關(guān)鍵字就直接用nonatomic
修飾濒持,那么這樣究竟會有什么問題呢键耕?看這樣一段代碼:
@property (nonatomic, strong) NSString *target;
dispatch_queue_t queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(100000, queue, ^(size_t i) {
self.target = [NSString stringWithFormat:@"abcdefghijk%zu",i];
});
這段代碼會有什么問題呢寺滚?運(yùn)行結(jié)果如下圖所示:
可以看到,因為對一個已經(jīng)釋放的對方調(diào)用了
release
方法屈雄,所以程序崩潰了村视。這是為什么呢?
我們知道在MRC
環(huán)境下酒奶,一個nonatomic
和retain
修飾的屬性的set方法其實等價于:
- (void)setTarget:(NSString *)target {
[target retain];//先保留新值
_target = target;//再進(jìn)行賦值
[_target release];//最后釋放舊值
}
可以想象到蚁孔,在多線程環(huán)境下,如果setTarget
這個方法在兩個線程中同時被調(diào)用惋嚎,那么很可能[_target release]
這行代碼在兩個線程中被連續(xù)調(diào)用兩次杠氢,因為nonatomic
沒有做任何的保護(hù),所以_target
指向的對象被連續(xù)釋放了兩次另伍,過度釋放引起crash
方案選擇
那么針對這種不安全的場景鼻百,我們該如何保護(hù)呢?
這里有一篇博客:GCD實踐(一)使用GCD保護(hù)property摆尝,作者總結(jié)了幾種方式温艇,比如atomic
,NSLock
,@synchronized
,GCD串行queue
,GCD并發(fā)隊列+barrier
等方式,并著重強(qiáng)調(diào)了GCD并發(fā)queue+barrier
能夠滿足多讀單寫的需求堕汞,性能比單純的串行隊列要好勺爱。但是并沒有對比其他幾種方式的性能。
其實并發(fā)隊列+barrier很多同學(xué)第一次看到應(yīng)該是在《Effective Objective-C 2.0》
這本書里吧讯检。一定會對這張圖印象深刻:
然而其實這種方式到底怎么好,究竟有多好人灼,其實我想大概很少有人真實的寫過demo去測試過吧
既然如此围段,那我們就驗證一下吧,畢竟理論是需要實踐和數(shù)據(jù)來支撐嘛
性能測試
其實如果不考慮任何具體的業(yè)務(wù)邏輯挡毅,僅僅測試各種加鎖方式的效率蒜撮,這里有一個測評:起底多線程同步鎖(iOS)
從測試數(shù)據(jù)中可以看到,atomic
加鎖方式的效率是最高,那如果加上具體的業(yè)務(wù)邏輯之后呢段磨?
廢話不多說取逾,我們還是直接寫demo測試下吧~
測試環(huán)境
iPhone6
真機(jī),10.3.3
系統(tǒng)苹支,ARC
內(nèi)存管理模式砾隅,dispatch_apply
+ 并發(fā) queue 執(zhí)行 10w
次,屬性的讀寫操作比例為9:1
债蜜,測試atomic
,NSLock
,并發(fā)queue+barrier
,等11種方案的效率晴埂,連續(xù)測試100次
關(guān)鍵測試代碼
#define TestThreadSafeMode(identifier,property,time,loop,ratio) \
@autoreleasepool { \
TICK(time, identifier); \
dispatch_apply(loop, self.barrierQueue, ^(size_t i) { \
if(!(i % ratio)) { \
self.property = [NSString stringWithFormat:@"abc%d",loop]; \
} else { \
__unused NSString * temp = self.property; \
} \
}); \
dispatch_barrier_sync(self.barrierQueue, ^{ \
TOCK(time, identifier); \
CALC(time, identifier); \
}); \
}
測試結(jié)果
測試結(jié)果如下(單位s):
ThreadSafeTest[408:46619] {
Atomic = "0.364874005317688";
Barrier = "7.570194065570831";
NSCondition = "6.852829992771149";
NSConditionLock = "6.897508859634399";
NSLock = "4.378997981548309";
NSRecursiveLock = "6.422177016735077";
PthreadMutex = "1.959827899932861";
RWLock = "0.7853890657424927";
Semaphore = "0.4313730001449585";
Synchronize = "6.652496039867401";
UnfairLock = "0.3706329464912415";
}
從測試數(shù)據(jù)中可以看到,atomic
加鎖方式的效率最高的寻定,并發(fā)隊列+barrier
方式竟然是最慢的儒洛,比atomic
慢了20倍。這個結(jié)果想必讓各位大跌眼鏡了吧狼速。其實我也是琅锻,直到我跑了很多遍之后才敢確認(rèn)。那么靜下來向胡,不禁要問一句:為什么atomic
能這么快呢恼蓬?atomic
關(guān)鍵字又是如何實現(xiàn)的呢?基于蘋果的開源代碼:accessors source code,我們可以找到set
方法的真正實現(xiàn):
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
可以看到僵芹,atomic
的實現(xiàn)主要依賴于自旋鎖处硬,而常見的NSLock
,pthread_mutex_t
等都是基于互斥鎖
他們的區(qū)別到底是什么呢?
- 自旋鎖是一種非阻塞鎖拇派,也就是說荷辕,如果某線程需要獲取自旋鎖,但該鎖已經(jīng)被其他線程占用時攀痊,該線程不會被掛起桐腌,而是在不斷的消耗
CPU
的時間,不停的試圖獲取自旋鎖 - 互斥鎖是阻塞鎖苟径,當(dāng)某線程無法獲取互斥量時案站,該線程會被直接掛起,該線程不再消耗
CPU
時間棘街,當(dāng)其他線程釋放互斥量后蟆盐,操作系統(tǒng)會激活那個被掛起的線程,讓其投入運(yùn)行
因此遭殉, 如果是多核處理器石挂,如果預(yù)計線程等待鎖的時間很短,短到比線程兩次上下文切換時間要少的情況下险污,使用自旋鎖是效率更高的痹愚。
看到這里富岳,雖然沒有直接的資料可以查詢到,不過也可以大概猜到并發(fā)queue+barrier
方式慢的原因了:因為讀操作和寫操作都需要配發(fā)到一個并發(fā)隊列中拯腮,那么最終執(zhí)行代碼的線程和最初調(diào)用代碼的線程很可能不是同一個窖式,這里有一次線程切換的開銷,如果這時候遇到互斥鎖需要等待的話动壤,當(dāng)前線程被掛起萝喘,再等到cpu喚醒代碼最終被執(zhí)行的時候一來一回又是兩次線程的切換,而對于很多輕量級的操作琼懊,這種線程之間切換的開銷要比自旋鎖的那種一直忙等待的方式慢很多阁簸。
總結(jié)
由上面的測試結(jié)論可見,atomic
的性能遠(yuǎn)遠(yuǎn)比我們想象中要好哼丈,并發(fā)queue+barrier
的方式遠(yuǎn)遠(yuǎn)比我們想象中要差启妹。但是atomic
真的是萬能的嗎?答案是否定的削祈。
atomic適用的場景
- 多線程環(huán)境下簡單對象屬性翅溺,僅有
set/get
的訪問操作 - 讀寫操作本身就很輕量,實際上只是簡單的讀寫實例變量
atomic不適用的場景
單線程環(huán)境髓抑,比如
UIKit
中所有類的屬性,因為不存在多線程競爭的問題优幸,加鎖會影響效率-
期望原子的操作是若干個
set
和get
方法的組合吨拍,比如i++
實際上等價于:int temp = i + 1; i = temp;
如果需要保證
i++
這個操作的線程安全相當(dāng)于set
和get
方法組合起來的原子性,而這是atomic
無法做到的測試代碼:
- (void)testComplex { int loop = 100000; //loop times dispatch_apply(loop, self.barrierQueue, ^(size_t i) { self.atomicNumber++; }); dispatch_barrier_sync(self.barrierQueue, ^{ NSLog(@"atomicNumber total:%lu", (unsigned long)self.atomicNumber); }); dispatch_apply(loop, self.barrierQueue, ^(size_t i) { [_lock lock]; self.nonatomicNumber++; [_lock unlock]; }); dispatch_barrier_sync(self.barrierQueue, ^{ NSLog(@"nonatomicNumber total:%lu", (unsigned long)self.nonatomicNumber); }); }
測試結(jié)果:
2017-09-05 14:42:39.598103+0800 ThreadSafeTest[420:52484] atomicNumber total:99281 2017-09-05 14:42:39.909207+0800 ThreadSafeTest[420:52484] nonatomicNumber total:100000
在這種場景下网杆,只能手動加鎖去保證羹饰,通過上面的測試結(jié)果可以看到,
@synchronized
和NSConditionLock
效率較差碳却,iOS10以后出的新自旋鎖unfairLock
和dispatch_semaphore
以及pthread_mutex_t
效率比較高队秩。推薦使用dispatch_semaphore_t
或者pthread_mutex_t
注:OSSpinLock
因為不再安全已經(jīng)被蘋果棄用,具體見郭曜源的這篇博客:不再安全的 OSSpinLock set
或者get
方法中有任意一個邏輯比較復(fù)雜需要手動重寫
因為atomic
修飾的屬性靠編譯器自動生成的get
和set
方法實現(xiàn)原子操作昼浦,如果重寫了任意一個馍资,atomic
關(guān)鍵字的特性將失效-
可變集合類對象的屬性
形如:@peroperty(atomic, strong) NSMutableArray * array; [self.array addObject:dummyObject];//線程不安全,在讀取array后关噪,執(zhí)行addObject 的過程中鸟蟹,array所指向的對象可能已經(jīng)在其他地方被釋放了
性能測試demo地址
A Test Project for Thread Safe Protection by 11 ways