你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 起底多線程同步鎖(iOS)

起底多線程同步鎖(iOS)

編輯:IOS開發基礎

36.jpg

本文為投稿文章,作者:SpringOx(博客)

iOS/MacOS為多線程、共享內存(變量)提供了多種的同步解決方案(即同步鎖),對於這些方案的比較,大都討論了鎖的用法以及鎖操作的開銷,然後就開銷表現排個序。春哥以為,最優方案的選用還是看應用場景,高頻接口PK低頻接口、有限沖突PK激烈競爭、代碼片段耗時的長短,以上都是正確選用的重要依據,不同方案在其適用范圍表現各有不同。這些方案當中,除了熟悉的iOS/MacOS系統自有的同步鎖,另外還有兩個自研的讀寫鎖,還有應用開發中常見的set/get訪問接口的原子操作屬性。

1、@synchronized(){}

Objective-C同步語法能夠實現對block內的代碼片段加鎖, 可以指定任意一個Objective-C對象(id指針)作為鎖“標記”,該語法將“標記”理解為token;

2、NSLock、NSRecursiveLock:

典型的面向對象的鎖,即同步鎖類,遵循Objective-C的NSLocking協議接口,前者支持tryLock,後者支持遞歸(可重入);

3、NSCondition、NSConditionLock:

基於信號量方式實現的鎖對象,前者提供單獨的信號量管理接口,相比後者用法上可以更為靈活,而後者在接口上更為直接、實用;

4、ANReadWriteLock、ANRecursiveRWLock:

iOS/MacOS並沒有提供讀寫鎖,春哥嘗試自己搞,Objective-C版的讀寫鎖(ANLock),遵循讀寫鎖特性,前者寫鎖耗時較小,後者支持遞歸;

5、pthread_mutex:

POSIX標准的unix多線程庫(pthread)中使用的互斥量,支持遞歸,需要特別說明的是信號機制pthread_cond_wait()同步方式也是依賴於該互斥量,pthread_cond_wait()本身並不具備同步能力;

6、dispatch_semaphore:

GCD用於控制多線程並發的信號量,允許通過wait/signal的信號事件控制並發執行的最大線程數,當最大線程數降級為1的時候則可當作同步鎖使用,注意該信號量並不支持遞歸;

7、OSSpinLock:

iOS/MacOS自有的自旋鎖,其特點是線程等待取鎖時不進內核,線程因此不掛起,直接保持空轉,這使得它的鎖操作開銷降得很低,OSSpinLock是不支持遞歸的;

8、atomic(property) set/get:

利用set/get接口的屬性實現原子操作,進而確保“被共享”的變量在多線程中讀寫安全,這已經是能滿足部分多線程同步要求;

基礎表現-鎖操作耗時:

千萬次鎖操作耗時單位秒-1024x499.png

上圖是常規的鎖操作性能測試(iOS7.0SDK,iPhone6模擬器,Yosemite 10.10.5),垂直方向表示耗時,單位是秒,總耗時越小越好,水平方向表示不同類型鎖的鎖操作,具體又分為兩部分,左邊的常規lock操作(比如NSLock)或者讀read操作(比如ANReadWriteLock),右邊則是寫write操作,圖上僅有ANReadWriteLock和ANRecursiveRWLock支持,其它不支持的則默認為0,圖上看出,單從性能表現,原子操作是表現最佳的(0.057412秒),@synchronized則是最耗時的(1.753565秒) (測試代碼) 

正如前文所述,不同方案各有側重,適用於不用的場景,不能唯性能論高低:

原子操作雖然性能很好,但僅限於set/get,比如對列表的插入移除操作需要做同步則無能為力,支持不到,所以適用於一些實例成員變量的讀寫同步;

得益於不進內核不掛起的方式,OSSpinLock有著優異的性能表現,然而在高並發執行(沖突概率大,競爭激烈)的時候,又或者代碼片段比較耗時(比如涉及內核執行文件io、socket、thread等),就容易引發CPU占有率暴漲的風險,因此更適用於一些簡短低耗時的代碼片段;

2016-01-03-at-11.48.16.png

2016-01-03-at-11.46.59.png

上圖為OSSpinLock等待取鎖時的耗時測試用例代碼,下圖為測試結果,圖中可以看到,等待取鎖時,如果異步線程比較耗時,CPU占有率會有一個飙升 (測試代碼)  

dispatch_semaphore的性能表現出乎意料之外的好,也沒有OSSpinLock的CPU占有率暴漲的問題,然而原本是用於GCD的多線程並發控制,也是信號量機制,是否適用於常規同步鎖有待實踐驗證,春哥這裡僅提供選擇,不做推薦;

21.png

上圖為dispatch_semaphore測試用例 

pthread_mutex是pthread經典的基於互斥量機制的同步鎖,特性、性能以及穩定各方面都已被大量項目所驗證,也是春哥比較推薦作為常規同步鎖首選;

22.png

上圖為pthread_mutex用法舉例

讀寫鎖的在鎖操作耗時上明顯不占優勢,讀寫鎖的主要性能優勢在於多線程高並發量的場景,這時候鎖競爭可能會非常激烈,使用一般的鎖這時候並發性能都會明顯下降,讀寫鎖對於所有讀操作能夠把同步放開,進而保持並發性能不受影響;以pthread_mutex和ANRecursiveRWLock為例,假設mutex的lock耗時為lk,則rw的read lock耗時為2.7lk(從性能測試圖表數據得出),read操作耗時為rd,1000次的多線程接口訪問:

mutex總耗時 = 1000*lk + 1000*rd

rw總耗時 = 1000*2.7*lk + 1000/c*rd

其中c表示應用的並發數,根據開發文檔和技術資料,iOS第二條線程起stack為512KB,而單個應用useable memory size在50MB以內,即c<=100;

假設線程數取中值c=50(嚴格來說,線程數不等於沖突計數,沖突計數很可能會比線程數小得多,線程同步運行不代表就即刻會發生沖突),當 mutex總耗時 > rw總耗時:

mutex總耗時 > rw總耗時  =》 50*lk + 50*rd > 50*2.7lk + rd  =》 49*rd > 85*lk   =》 rd > 1.73*lk

可以看出,只要read操作耗時超過鎖操作耗時的1.7倍(這其實很容易達到的),讀寫鎖的性能就會占優勢

假設線程數c=2(如上述,這裡是假設了兩個線程之間是競爭了,發生沖突,實際未必):

mutex總耗時 > rw總耗時  =》 2*lk + 2*rd > 5.4*lk + rd  =》 rd > 3.4lk

即使只有兩個並發線程,只要read操作耗時超過鎖操作耗時的3.4倍,讀寫鎖的性能還會占優勢

假設線程數c=1:

mutex總耗時 > rw總耗時  =》0 > 1.7lk

這顯然不成立,說明當單個線程的時候,rw的性能不可能有優勢。這也好理解,這時候的mutex和rw的讀操作都相當完全同步,不論是mutex還是rw,性能完全取決於鎖操作本身,而rw在鎖操作耗時上就不占優勢,所以mutex總耗時總是要小於rw總耗時的。

025.png

026.png

027.png

上圖是mutex鎖和rw鎖read操作的耗時測試用例,下圖為測試結果,read操作設置為100微秒,mutex鎖的總耗時是rw鎖的5倍多,read操作的耗時遠比鎖操作大許多(2k倍),根據上述恆等式計算可以得出實際的沖突計數c=5 (測試代碼)  

其它方案的討論:

a、NSCondition和NSConditionLock實際使用的性能表現並任何優勢,然而條件鎖的意義在於對信號量做了面向對象封裝;

b、NSLock和NSRecursiveLock在性能表現上與mutex算比較接近,用法上也並無二致,因此,常規情況,NSRecursiveLock和mutex之間的選擇,春哥以為更多是習慣和偏好的問題;

c、@synchronized似乎是這些方案當中性能表現最不佳的,那是不是應該完全拋棄呢?春哥倒不這麼認為,@synchronized最大的特點在於“快捷”,同步語法僅僅需要一個對象(id指針)作為互斥量,而且還不限於實例對象,類對象也能夠支持,這就使得類方法中做同步變得簡單不少,block用法也使得代碼更緊湊,內存管理更穩健,非常適合一些低頻而又不得不同步的邏輯,比如單例初始化、啟動加載等等。

綜合上述分析與討論,總結有以下幾點原則:

1、總的來看,推薦pthread_mutex作為實際項目的首選方案;

2、對於耗時較大又易沖突的讀操作,可以使用讀寫鎖代替pthread_mutex;

3、如果確認僅有set/get的訪問操作,可以選用原子操作屬性;

4、對於性能要求苛刻,可以考慮使用OSSpinLock,需要確保加鎖片段的耗時足夠小;

5、條件鎖基本上使用面向對象的NSCondition和NSConditionLock即可;

6、@synchronized則適用於低頻場景如初始化或者緊急修復使用;

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved