iOS多线程同步
本文為轉(zhuǎn)載文章,作者:SpringOx( 博客)原文鏈接http://www.cocoachina.com/ios/20160129/15170.html
iOS/MacOS為多線程、共享內(nèi)存(變量)提供了多種的同步解決方案(即同步鎖),對(duì)于這些方案的比較,大都討論了鎖的用法以及鎖操作的開(kāi)銷(xiāo),然后就開(kāi)銷(xiāo)表現(xiàn)排個(gè)序。春哥以為,最優(yōu)方案的選用還是看應(yīng)用場(chǎng)景,高頻接口PK低頻接口、有限沖突PK激烈競(jìng)爭(zhēng)、代碼片段耗時(shí)的長(zhǎng)短,以上都是正確選用的重要依據(jù),不同方案在其適用范圍表現(xiàn)各有不同。這些方案當(dāng)中,除了熟悉的iOS/MacOS系統(tǒng)自有的同步鎖,另外還有兩個(gè)自研的讀寫(xiě)鎖,還有應(yīng)用開(kāi)發(fā)中常見(jiàn)的set/get訪問(wèn)接口的原子操作屬性。
1、@synchronized(){}
Objective-C同步語(yǔ)法能夠?qū)崿F(xiàn)對(duì)block內(nèi)的代碼片段加鎖, 可以指定任意一個(gè)Objective-C對(duì)象(id指針)作為鎖“標(biāo)記”,該語(yǔ)法將“標(biāo)記”理解為token;
2、NSLock、NSRecursiveLock:
典型的面向?qū)ο蟮逆i,即同步鎖類(lèi),遵循Objective-C的NSLocking協(xié)議接口,前者支持tryLock,后者支持遞歸(可重入);
3、NSCondition、NSConditionLock:
基于信號(hào)量方式實(shí)現(xiàn)的鎖對(duì)象,前者提供單獨(dú)的信號(hào)量管理接口,相比后者用法上可以更為靈活,而后者在接口上更為直接、實(shí)用;
4、ANReadWriteLock、ANRecursiveRWLock:
iOS/MacOS并沒(méi)有提供讀寫(xiě)鎖,春哥嘗試自己搞,Objective-C版的讀寫(xiě)鎖(ANLock),遵循讀寫(xiě)鎖特性,前者寫(xiě)鎖耗時(shí)較小,后者支持遞歸;
5、pthread_mutex:
POSIX標(biāo)準(zhǔn)的unix多線程庫(kù)(pthread)中使用的互斥量,支持遞歸,需要特別說(shuō)明的是信號(hào)機(jī)制pthread_cond_wait()同步方式也是依賴(lài)于該互斥量,pthread_cond_wait()本身并不具備同步能力;
6、dispatch_semaphore:
GCD用于控制多線程并發(fā)的信號(hào)量,允許通過(guò)wait/signal的信號(hào)事件控制并發(fā)執(zhí)行的最大線程數(shù),當(dāng)最大線程數(shù)降級(jí)為1的時(shí)候則可當(dāng)作同步鎖使用,注意該信號(hào)量并不支持遞歸;
7、OSSpinLock:
iOS/MacOS自有的自旋鎖,其特點(diǎn)是線程等待取鎖時(shí)不進(jìn)內(nèi)核,線程因此不掛起,直接保持空轉(zhuǎn),這使得它的鎖操作開(kāi)銷(xiāo)降得很低,OSSpinLock是不支持遞歸的;
8、atomic(property) set/get:
利用set/get接口的屬性實(shí)現(xiàn)原子操作,進(jìn)而確保“被共享”的變量在多線程中讀寫(xiě)安全,這已經(jīng)是能滿足部分多線程同步要求;
基礎(chǔ)表現(xiàn)-鎖操作耗時(shí):
上圖是常規(guī)的鎖操作性能測(cè)試(iOS7.0SDK,iPhone6模擬器,Yosemite 10.10.5),垂直方向表示耗時(shí),單位是秒,總耗時(shí)越小越好,水平方向表示不同類(lèi)型鎖的鎖操作,具體又分為兩部分,左邊的常規(guī)lock操作(比如NSLock)或者讀read操作(比如ANReadWriteLock),右邊則是寫(xiě)write操作,圖上僅有ANReadWriteLock和ANRecursiveRWLock支持,其它不支持的則默認(rèn)為0,圖上看出,單從性能表現(xiàn),原子操作是表現(xiàn)最佳的(0.057412秒),@synchronized則是最耗時(shí)的(1.753565秒) (測(cè)試代碼)?
正如前文所述,不同方案各有側(cè)重,適用于不用的場(chǎng)景,不能唯性能論高低:
原子操作雖然性能很好,但僅限于set/get,比如對(duì)列表的插入移除操作需要做同步則無(wú)能為力,支持不到,所以適用于一些實(shí)例成員變量的讀寫(xiě)同步;
得益于不進(jìn)內(nèi)核不掛起的方式,OSSpinLock有著優(yōu)異的性能表現(xiàn),然而在高并發(fā)執(zhí)行(沖突概率大,競(jìng)爭(zhēng)激烈)的時(shí)候,又或者代碼片段比較耗時(shí)(比如涉及內(nèi)核執(zhí)行文件io、socket、thread等),就容易引發(fā)CPU占有率暴漲的風(fēng)險(xiǎn),因此更適用于一些簡(jiǎn)短低耗時(shí)的代碼片段;
上圖為OSSpinLock等待取鎖時(shí)的耗時(shí)測(cè)試用例代碼,下圖為測(cè)試結(jié)果,圖中可以看到,等待取鎖時(shí),如果異步線程比較耗時(shí),CPU占有率會(huì)有一個(gè)飆升 (測(cè)試代碼) ?
dispatch_semaphore的性能表現(xiàn)出乎意料之外的好,也沒(méi)有OSSpinLock的CPU占有率暴漲的問(wèn)題,然而原本是用于GCD的多線程并發(fā)控制,也是信號(hào)量機(jī)制,是否適用于常規(guī)同步鎖有待實(shí)踐驗(yàn)證,春哥這里僅提供選擇,不做推薦;
上圖為dispatch_semaphore測(cè)試用例?
pthread_mutex是pthread經(jīng)典的基于互斥量機(jī)制的同步鎖,特性、性能以及穩(wěn)定各方面都已被大量項(xiàng)目所驗(yàn)證,也是春哥比較推薦作為常規(guī)同步鎖首選;
上圖為pthread_mutex用法舉例
讀寫(xiě)鎖的在鎖操作耗時(shí)上明顯不占優(yōu)勢(shì),讀寫(xiě)鎖的主要性能優(yōu)勢(shì)在于多線程高并發(fā)量的場(chǎng)景,這時(shí)候鎖競(jìng)爭(zhēng)可能會(huì)非常激烈,使用一般的鎖這時(shí)候并發(fā)性能都會(huì)明顯下降,讀寫(xiě)鎖對(duì)于所有讀操作能夠把同步放開(kāi),進(jìn)而保持并發(fā)性能不受影響;以pthread_mutex和ANRecursiveRWLock為例,假設(shè)mutex的lock耗時(shí)為lk,則rw的read lock耗時(shí)為2.7lk(從性能測(cè)試圖表數(shù)據(jù)得出),read操作耗時(shí)為rd,1000次的多線程接口訪問(wèn):
mutex總耗時(shí) = 1000*lk + 1000*rd
rw總耗時(shí) = 1000*2.7*lk + 1000/c*rd
其中c表示應(yīng)用的并發(fā)數(shù),根據(jù)開(kāi)發(fā)文檔和技術(shù)資料,iOS第二條線程起stack為512KB,而單個(gè)應(yīng)用useable memory size在50MB以內(nèi),即c<=100;
假設(shè)線程數(shù)取中值c=50(嚴(yán)格來(lái)說(shuō),線程數(shù)不等于沖突計(jì)數(shù),沖突計(jì)數(shù)很可能會(huì)比線程數(shù)小得多,線程同步運(yùn)行不代表就即刻會(huì)發(fā)生沖突),當(dāng) mutex總耗時(shí) > rw總耗時(shí):
mutex總耗時(shí) > rw總耗時(shí) ?=》 50*lk + 50*rd > 50*2.7lk + rd ?=》 49*rd > 85*lk ? =》 rd > 1.73*lk
可以看出,只要read操作耗時(shí)超過(guò)鎖操作耗時(shí)的1.7倍(這其實(shí)很容易達(dá)到的),讀寫(xiě)鎖的性能就會(huì)占優(yōu)勢(shì)
假設(shè)線程數(shù)c=2(如上述,這里是假設(shè)了兩個(gè)線程之間是競(jìng)爭(zhēng)了,發(fā)生沖突,實(shí)際未必):
mutex總耗時(shí) > rw總耗時(shí) ?=》 2*lk + 2*rd > 5.4*lk + rd ?=》 rd > 3.4lk
即使只有兩個(gè)并發(fā)線程,只要read操作耗時(shí)超過(guò)鎖操作耗時(shí)的3.4倍,讀寫(xiě)鎖的性能還會(huì)占優(yōu)勢(shì)
假設(shè)線程數(shù)c=1:
mutex總耗時(shí) > rw總耗時(shí) ?=》0 > 1.7lk
這顯然不成立,說(shuō)明當(dāng)單個(gè)線程的時(shí)候,rw的性能不可能有優(yōu)勢(shì)。這也好理解,這時(shí)候的mutex和rw的讀操作都相當(dāng)完全同步,不論是mutex還是rw,性能完全取決于鎖操作本身,而rw在鎖操作耗時(shí)上就不占優(yōu)勢(shì),所以mutex總耗時(shí)總是要小于rw總耗時(shí)的。
上圖是mutex鎖和rw鎖read操作的耗時(shí)測(cè)試用例,下圖為測(cè)試結(jié)果,read操作設(shè)置為100微秒,mutex鎖的總耗時(shí)是rw鎖的5倍多,read操作的耗時(shí)遠(yuǎn)比鎖操作大許多(2k倍),根據(jù)上述恒等式計(jì)算可以得出實(shí)際的沖突計(jì)數(shù)c=5 (測(cè)試代碼) ?
其它方案的討論:
a、NSCondition和NSConditionLock實(shí)際使用的性能表現(xiàn)并任何優(yōu)勢(shì),然而條件鎖的意義在于對(duì)信號(hào)量做了面向?qū)ο蠓庋b;
b、NSLock和NSRecursiveLock在性能表現(xiàn)上與mutex算比較接近,用法上也并無(wú)二致,因此,常規(guī)情況,NSRecursiveLock和mutex之間的選擇,春哥以為更多是習(xí)慣和偏好的問(wèn)題;
c、@synchronized似乎是這些方案當(dāng)中性能表現(xiàn)最不佳的,那是不是應(yīng)該完全拋棄呢?春哥倒不這么認(rèn)為,@synchronized最大的特點(diǎn)在于“快捷”,同步語(yǔ)法僅僅需要一個(gè)對(duì)象(id指針)作為互斥量,而且還不限于實(shí)例對(duì)象,類(lèi)對(duì)象也能夠支持,這就使得類(lèi)方法中做同步變得簡(jiǎn)單不少,block用法也使得代碼更緊湊,內(nèi)存管理更穩(wěn)健,非常適合一些低頻而又不得不同步的邏輯,比如單例初始化、啟動(dòng)加載等等。
綜合上述分析與討論,總結(jié)有以下幾點(diǎn)原則:
1、總的來(lái)看,推薦pthread_mutex作為實(shí)際項(xiàng)目的首選方案;
2、對(duì)于耗時(shí)較大又易沖突的讀操作,可以使用讀寫(xiě)鎖代替pthread_mutex;
3、如果確認(rèn)僅有set/get的訪問(wèn)操作,可以選用原子操作屬性;
4、對(duì)于性能要求苛刻,可以考慮使用OSSpinLock,需要確保加鎖片段的耗時(shí)足夠小;
5、條件鎖基本上使用面向?qū)ο蟮腘SCondition和NSConditionLock即可;
6、@synchronized則適用于低頻場(chǎng)景如初始化或者緊急修復(fù)使用;
總結(jié)
- 上一篇: CISAW培训可以自学报名考试吗?
- 下一篇: 自然语言处理笔记9-哈工大 关毅