iOS 如何高效的使用多線程
一、多線程簡(jiǎn)述
線程是程序執(zhí)行流的最小單元,一個(gè)線程包括:獨(dú)有ID,程序計(jì)數(shù)器 (Program Counter),寄存器集合,堆棧。同一進(jìn)程可以有多個(gè)線程,它們共享進(jìn)程的全局變量和堆數(shù)據(jù)。
這里的 PC (Program Counter) 指向的是當(dāng)前的指令地址,通過(guò) PC 的更新來(lái)運(yùn)行我們的程序,一個(gè)線程同一時(shí)刻只能執(zhí)行一條指令。當(dāng)然我們知道線程和進(jìn)程都是虛擬的概念,實(shí)際上 PC 是 CPU 核心中的寄存器,它是實(shí)際存在的,所以也可以說(shuō)一個(gè) CPU 核心同一時(shí)刻只能執(zhí)行一個(gè)線程。
不管是多處理器設(shè)備還是多核設(shè)備,開(kāi)發(fā)者往往只需要關(guān)心 CPU 的核心數(shù)量,而不需關(guān)心它們的物理構(gòu)成。CPU 核心數(shù)量是有限的,也就是說(shuō)一個(gè)設(shè)備并發(fā)執(zhí)行的線程數(shù)量是有限的,當(dāng)線程數(shù)量超過(guò) CPU 核心數(shù)量時(shí),一個(gè) CPU 核心往往就要處理多個(gè)線程,這個(gè)行為叫做線程調(diào)度。
線程調(diào)度簡(jiǎn)單來(lái)說(shuō)就是:一個(gè) CPU 核心輪流讓各個(gè)線程分別執(zhí)行一段時(shí)間。當(dāng)然這中間還包含著復(fù)雜的邏輯,后文再來(lái)分析。
二、多線程的優(yōu)化思路
在移動(dòng)端開(kāi)發(fā)中,因?yàn)橄到y(tǒng)的復(fù)雜性,開(kāi)發(fā)者往往不能期望所有線程都能真正的并發(fā)執(zhí)行,而且開(kāi)發(fā)者也不清楚 XNU 何時(shí)切換內(nèi)核態(tài)線程、何時(shí)進(jìn)行線程調(diào)度,所以開(kāi)發(fā)者要經(jīng)??紤]到線程調(diào)度的情況。
1、減少線程切換
當(dāng)線程數(shù)量超過(guò) CPU 核心數(shù)量,CPU 核心通過(guò)線程調(diào)度切換用戶(hù)態(tài)線程,意味著有上下文的轉(zhuǎn)換(寄存器數(shù)據(jù)、棧等),過(guò)多的上下文切換會(huì)帶來(lái)資源開(kāi)銷(xiāo)。雖然內(nèi)核態(tài)線程的切換理論上不會(huì)是性能負(fù)擔(dān),開(kāi)發(fā)中還是應(yīng)該盡量減少線程的切換。
2、線程優(yōu)先級(jí)權(quán)衡
通常來(lái)說(shuō),線程調(diào)度除了輪轉(zhuǎn)法以外,還有優(yōu)先級(jí)調(diào)度的方案,在線程調(diào)度時(shí),高優(yōu)先級(jí)的線程會(huì)更早的執(zhí)行。有兩個(gè)概念需要明確:
- IO 密集型線程:頻繁等待的線程,等待的時(shí)候會(huì)讓出時(shí)間片。
- CPU 密集型線程:很少等待的線程,意味著長(zhǎng)時(shí)間占用著 CPU。
特殊場(chǎng)景下,當(dāng)多個(gè) CPU 密集型線程霸占了所有 CPU 資源,而它們的優(yōu)先級(jí)都比較高,而此時(shí)優(yōu)先級(jí)較低的 IO 密集型線程將持續(xù)等待,產(chǎn)生線程餓死的現(xiàn)象。當(dāng)然,為了避免線程餓死,系統(tǒng)會(huì)逐步提高被“冷落”線程的優(yōu)先級(jí),IO 密集型線程通常情況下比 CPU 密集型線程更容易獲取到優(yōu)先級(jí)提升。
雖然系統(tǒng)會(huì)自動(dòng)做這些事情,但是這總歸會(huì)造成時(shí)間等待,可能會(huì)影響用戶(hù)體驗(yàn)。所以筆者認(rèn)為開(kāi)發(fā)者需要從兩個(gè)方面權(quán)衡優(yōu)先級(jí)問(wèn)題:
- 讓 IO 密集型線程優(yōu)先級(jí)高于 CPU 密集型線程。
- 讓緊急的任務(wù)擁有更高的優(yōu)先級(jí)。
比如一個(gè)場(chǎng)景:大量的圖片異步解壓的任務(wù),解壓的圖片不需要立即反饋給用戶(hù),同時(shí)又有大量的異步查詢(xún)磁盤(pán)緩存的任務(wù),而查詢(xún)磁盤(pán)緩存任務(wù)完成過(guò)后需要反饋給用戶(hù)。
圖片解壓屬于 CPU 密集型線程,查詢(xún)磁盤(pán)緩存屬于 IO 密集型線程,而后者需要反饋給用戶(hù)更加緊急,所以應(yīng)該讓圖片解壓線程的優(yōu)先級(jí)低一點(diǎn),查詢(xún)磁盤(pán)緩存的線程優(yōu)先級(jí)高一點(diǎn)。
值得注意的是,這里是說(shuō)大量的異步任務(wù),意味著 CPU 很有可能滿(mǎn)負(fù)荷運(yùn)算,若 CPU 資源綽綽有余的情況下就沒(méi)那個(gè)必要去處理優(yōu)先級(jí)問(wèn)題。
3、主線程任務(wù)的優(yōu)化
有些業(yè)務(wù)只能寫(xiě)在主線程,比如 UI 類(lèi)組件的初始化及其布局。其實(shí)這方面的優(yōu)化就比較多了,業(yè)界所說(shuō)的性能優(yōu)化大部分都是為了減輕主線程的壓力,似乎有些偏離了多線程優(yōu)化的范疇了,下面就基于主線程任務(wù)的管理大致羅列幾點(diǎn)吧:
- 內(nèi)存復(fù)用
通過(guò)內(nèi)存復(fù)用來(lái)減少開(kāi)辟內(nèi)存的時(shí)間消耗,這在系統(tǒng) UI 類(lèi)組件中應(yīng)用廣泛,比如 UITableViewCell 的復(fù)用。同時(shí),減少開(kāi)辟內(nèi)存意味著減少了內(nèi)存釋放,同樣能節(jié)約 CPU 資源。
- 懶加載任務(wù)
既然 UI 組件必須在主線程初始化,那么就需要用時(shí)再初始化吧,swift 的寫(xiě)時(shí)復(fù)制也是類(lèi)似的思路。
- 任務(wù)拆分排隊(duì)執(zhí)行
通過(guò)監(jiān)聽(tīng) Runloop 即將結(jié)束等通知,將大量的任務(wù)拆分開(kāi)來(lái),在每次 Runloop 循環(huán)周期執(zhí)行少量任務(wù)。其實(shí)在實(shí)踐這種優(yōu)化思路之前,應(yīng)該想想能不能將任務(wù)放到異步線程,而不是用這種比較極端的優(yōu)化手段。
- 主線程空閑時(shí)執(zhí)行任務(wù)
//這里是主線程上下文 `dispatch_async(dispatch_get_main_queue(), ^{ //等到主線程空閑執(zhí)行該任務(wù) });
三、關(guān)于“鎖”
多線程會(huì)帶來(lái)線程安全問(wèn)題,當(dāng)原子操作不能滿(mǎn)足業(yè)務(wù)時(shí),往往需要使用各種“鎖”來(lái)保證內(nèi)存的讀寫(xiě)安全。
常用的鎖有互斥鎖、讀寫(xiě)鎖、空轉(zhuǎn)鎖,通常情況下,iOS 開(kāi)發(fā)中互斥鎖pthread_mutex_t、dispatch_semaphore_t,讀寫(xiě)鎖pthread_rwlock_t就能滿(mǎn)足大部分需求,并且性能不錯(cuò)。
在讀取鎖失敗時(shí),線程有可能有兩種狀態(tài):
- 空轉(zhuǎn)狀態(tài):線程執(zhí)行空任務(wù)循環(huán)等待,當(dāng)鎖可用時(shí)立即獲取鎖。
- 掛起狀態(tài):線程掛起,當(dāng)鎖可用時(shí)需要其他線程喚醒。
喚醒線程比較耗時(shí),線程空轉(zhuǎn)需要消耗 CPU 資源并且時(shí)間越長(zhǎng)消耗越多,由此可知空轉(zhuǎn)適合少量任務(wù)、掛起適合大量任務(wù)。
實(shí)際上互斥鎖和讀寫(xiě)鎖都有空轉(zhuǎn)鎖的特性,它們?cè)讷@取鎖失敗時(shí)會(huì)先空轉(zhuǎn)一段時(shí)間,然后才會(huì)掛起,而空轉(zhuǎn)鎖也不會(huì)永遠(yuǎn)的空轉(zhuǎn),在特定的空轉(zhuǎn)時(shí)間過(guò)后仍然會(huì)掛起,所以通常情況下不用刻意去使用空轉(zhuǎn)鎖,Casa Taloyum 在博客中有詳細(xì)的解釋。
1、OSSpinLock 優(yōu)先級(jí)反轉(zhuǎn)問(wèn)題
優(yōu)先級(jí)反轉(zhuǎn)概念:比如兩個(gè)線程 A 和 B,優(yōu)先級(jí) A < B。當(dāng) A 獲取鎖訪問(wèn)共享資源時(shí),B 嘗試獲取鎖,那么 B 就會(huì)進(jìn)入忙等狀態(tài),忙等時(shí)間越長(zhǎng)對(duì) CPU 資源的占用越大;而由于 A 的優(yōu)先級(jí)低于 B,A 無(wú)法與高優(yōu)先級(jí)的線程爭(zhēng)奪 CPU 資源,從而導(dǎo)致任務(wù)遲遲完成不了。解決優(yōu)先級(jí)反轉(zhuǎn)的方法有“優(yōu)先級(jí)天花板”和“優(yōu)先級(jí)繼承”,它們的核心操作都是提升當(dāng)前正在訪問(wèn)共享資源的線程的優(yōu)先級(jí)。
2、避免死鎖
很常見(jiàn)的場(chǎng)景是,同一線程重復(fù)獲取鎖導(dǎo)致的死鎖,這種情況可以使用遞歸鎖來(lái)處理,pthread_mutex_t使用pthread_mutex_init_recursive()方法初始化就能擁有遞歸鎖的特性。
使用pthread_mutex_trylock()等嘗試獲取鎖的方法能有效的避免死鎖的情況
3、最小化加鎖任務(wù)
開(kāi)發(fā)者應(yīng)該充分的理解業(yè)務(wù),將鎖包含的代碼區(qū)域盡量縮小,不會(huì)出現(xiàn)線程安全問(wèn)題 的代碼就不要用鎖來(lái)保護(hù)了,這樣才能提高并發(fā)時(shí)鎖的性能。
4、時(shí)刻注意不可重入方法的安全
當(dāng)一個(gè)方法是可重入的時(shí)候,可以放心大膽的使用,若一個(gè)方法不可重入,開(kāi)發(fā)者應(yīng)該多留意,思考這個(gè)方法會(huì)不會(huì)有多個(gè)線程訪問(wèn)的情況,若有就老老實(shí)實(shí)的加上線程鎖。
5、編譯器的過(guò)度優(yōu)化
編譯器可能會(huì)為了提高效率將變量寫(xiě)入寄存器而暫時(shí)不寫(xiě)回,方便下次使用,我們知道一句代碼轉(zhuǎn)換為指令不止一條,所以在變量寫(xiě)入寄存器沒(méi)來(lái)得及寫(xiě)回的過(guò)程中,可能這個(gè)變量被其它線程讀寫(xiě)了。編譯器同樣會(huì)為了提高效率對(duì)它認(rèn)為順序無(wú)關(guān)的指令調(diào)換順序。
以上都可能會(huì)導(dǎo)致合理使用鎖的地方仍然線程不安全,而volatile關(guān)鍵字就可以解決這類(lèi)問(wèn)題,它能阻止編譯器為了效率將變量緩存到寄存器而不及時(shí)寫(xiě)回,也能阻止編譯器調(diào)整操作volatile修飾變量的指令順序。
原子自增函數(shù)就有類(lèi)似的應(yīng)用: int32_t OSAtomicIncrement32( volatile int32_t *__theValue )
。
CPU 也可能為了提高效率而去交換指令的順序,導(dǎo)致加鎖的代碼也不安全,解決這類(lèi)問(wèn)題可以使用內(nèi)存屏障,CPU 越過(guò)內(nèi)存屏障后會(huì)刷新寄存器對(duì)變量的分配。
以上就是iOS 如何高效使用的多線程的詳細(xì)內(nèi)容,更多關(guān)于ios 多線程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- IOS開(kāi)發(fā)之多線程N(yùn)SThiread GCD NSOperation Runloop
- iOS實(shí)現(xiàn)音樂(lè)播放器圖片旋轉(zhuǎn)
- iOS實(shí)現(xiàn)電子簽名
- iOS WKWebview 白屏檢測(cè)實(shí)現(xiàn)的示例
- iOS實(shí)現(xiàn)折疊單元格
- iOS藍(lán)牙設(shè)備名稱(chēng)緩存問(wèn)題的解決方法
- iOS如何開(kāi)發(fā)簡(jiǎn)單的手繪應(yīng)用實(shí)例詳解
- iOS程序性能優(yōu)化的技巧
- iOS中各種UI控件屬性設(shè)置示例代碼
- 分析IOS RunLoop的事件循環(huán)機(jī)制
相關(guān)文章
詳解iOS多線程之2.NSThread的加鎖@synchronized
這篇文章主要介紹了詳解iOS多線程之2.NSThread的加鎖@synchronized,有需要的小伙伴可以參考下。2016-11-11iOS CoreMotion實(shí)現(xiàn)設(shè)備運(yùn)動(dòng)加速度計(jì)陀螺儀
這篇文章主要介紹了iOS CoreMotion實(shí)現(xiàn)設(shè)備運(yùn)動(dòng)加速度計(jì)陀螺儀,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12iOS App中調(diào)用相冊(cè)中圖片及獲取最近的一張圖片的方法
這篇文章主要介紹了iOS App中調(diào)用相冊(cè)中圖片及獲取最近的一張圖片的方法,示例代碼為傳統(tǒng)的Objective-C語(yǔ)言,需要的朋友可以參考下2016-03-03iOS系統(tǒng)和微信中不支持audio自動(dòng)播放問(wèn)題的解決方法
最近在微信端開(kāi)發(fā)H5的時(shí)候,audio標(biāo)簽在蘋(píng)果機(jī)上無(wú)法進(jìn)行自動(dòng)播放,查找相關(guān)資料終于解決了,所以下面這篇文章主要給大家介紹了關(guān)于iOS系統(tǒng)和微信中不支持audio自動(dòng)播放問(wèn)題的解決方法,需要的朋友可以參考下。2017-09-09將多個(gè)字符串高亮顯示之TTTAttributedLabel
本文介紹了將多個(gè)字符串高亮顯示之TTTAttributedLabel。在此需要對(duì)每個(gè)字符串進(jìn)行匹配,可以研究下kmp和bm算法,在這里應(yīng)用了oc自帶的NSRegularExpression 來(lái)進(jìn)行正則表達(dá)式匹配,算是比較簡(jiǎn)單的方法,需要的朋友可以參考下2015-07-07iphone的safari瀏覽器中實(shí)現(xiàn)全屏瀏覽的方法
這篇文章主要介紹了iphone的safari瀏覽器中實(shí)現(xiàn)全屏瀏覽的方法,同時(shí)介紹了Add to Home Screen功能的實(shí)現(xiàn)方法,需要的朋友可以參考下2014-06-06iOS實(shí)現(xiàn)可以縱向橫向滑動(dòng)的表格實(shí)例代碼
這篇文章主要給大家介紹了利用iOS實(shí)現(xiàn)可以縱向橫向滑動(dòng)的表格的相關(guān)資料,文中給出了詳細(xì)的實(shí)現(xiàn)方法示例代碼,對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-06-06iOS10開(kāi)發(fā)和Xcode 8新特性及常見(jiàn)問(wèn)題解析
這篇文章主要介紹了iOS10開(kāi)發(fā)和Xcode 8新特性及常見(jiàn)問(wèn)題解析的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09iOS簡(jiǎn)單到無(wú)門(mén)檻調(diào)試WebView的步驟詳解
這篇文章主要給大家介紹了關(guān)于iOS調(diào)試WebView的相關(guān)資料,文中介紹的方法可以說(shuō)是非常簡(jiǎn)單,簡(jiǎn)單到無(wú)門(mén)檻,通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07設(shè)計(jì)模式中的迭代器模式在Cocoa Touch框架中的使用
這篇文章主要介紹了設(shè)計(jì)模式中的迭代器模式在Cocoa Touch框架中的使用,示例代碼為傳統(tǒng)的Objective-C,需要的朋友可以參考下2016-03-03