atomic性能真的很差院崇,并發(fā)queue+barrier性能真的很好嗎?

前言

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é)果如下圖所示:

crash.png

可以看到,因為對一個已經(jīng)釋放的對方調(diào)用了release方法屈雄,所以程序崩潰了村视。這是為什么呢?

我們知道在MRC環(huán)境下酒奶,一個nonatomicretain修飾的屬性的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》這本書里吧讯检。一定會對這張圖印象深刻:

第41條:多用派發(fā)隊列琐鲁,少用同步鎖.png

然而其實這種方式到底怎么好,究竟有多好人灼,其實我想大概很少有人真實的寫過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中所有類的屬性,因為不存在多線程競爭的問題优幸,加鎖會影響效率

  • 期望原子的操作是若干個setget方法的組合吨拍,比如 i++實際上等價于:

    int temp = i + 1; i = temp; 
    

    如果需要保證i++這個操作的線程安全相當(dāng)于setget方法組合起來的原子性,而這是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é)果可以看到,@synchronizedNSConditionLock效率較差碳却,iOS10以后出的新自旋鎖unfairLockdispatch_semaphore以及pthread_mutex_t效率比較高队秩。推薦使用dispatch_semaphore_t或者pthread_mutex_t
    注:OSSpinLock因為不再安全已經(jīng)被蘋果棄用,具體見郭曜源的這篇博客:不再安全的 OSSpinLock

  • set或者get方法中有任意一個邏輯比較復(fù)雜需要手動重寫
    因為atomic修飾的屬性靠編譯器自動生成的getset方法實現(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

本文參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市使兔,隨后出現(xiàn)的幾起案子建钥,更是在濱河造成了極大的恐慌,老刑警劉巖虐沥,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件熊经,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)镐依,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門悉盆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人馋吗,你說我怎么就攤上這事焕盟。” “怎么了宏粤?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵脚翘,是天一觀的道長。 經(jīng)常有香客問我绍哎,道長来农,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任崇堰,我火速辦了婚禮沃于,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘海诲。我一直安慰自己繁莹,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布特幔。 她就那樣靜靜地躺著咨演,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蚯斯。 梳的紋絲不亂的頭發(fā)上薄风,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機(jī)與錄音拍嵌,去河邊找鬼遭赂。 笑死,一個胖子當(dāng)著我的面吹牛横辆,可吹牛的內(nèi)容都是我干的撇他。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼龄糊,長吁一口氣:“原來是場噩夢啊……” “哼逆粹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起炫惩,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤僻弹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后他嚷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蹋绽,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡芭毙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了卸耘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片退敦。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蚣抗,靈堂內(nèi)的尸體忽然破棺而出侈百,到底是詐尸還是另有隱情,我是刑警寧澤翰铡,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布钝域,位于F島的核電站,受9級特大地震影響锭魔,放射性物質(zhì)發(fā)生泄漏例证。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一迷捧、第九天 我趴在偏房一處隱蔽的房頂上張望织咧。 院中可真熱鬧,春花似錦漠秋、人聲如沸笙蒙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽手趣。三九已至,卻和暖如春肥荔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背朝群。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工燕耿, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人姜胖。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓誉帅,卻偏偏與公主長得像,于是被迫代替她去往敵國和親右莱。 傳聞我的和親對象是個殘疾皇子蚜锨,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容