收錄:原文地址html
多線程技術在移動端開發中應用普遍,GCD 讓 iOS 開發者能輕易的使用多線程,然而這並不意味着代碼就必定高效和可靠。深刻理解其原理並常常結合業務思考,才能在有限的線程控制 API 中最大化發揮併發編程的能力,也能輕易的察覺到代碼可能存在的安全問題並優雅的解決它。ios
本文不會講解 GCD 和各類「鎖」的基本用法,而是結合操做系統的一些知識和筆者的認識講述偏「思惟」的東西,固然,最終也是爲了能更高效的應用多線程。編程
行文可能有誤歡迎指出錯誤。swift
線程是程序執行流的最小單元,一個線程包括:獨有ID,程序計數器 (Program Counter),寄存器集合,堆棧。同一進程能夠有多個線程,它們共享進程的全局變量和堆數據。數組
這裏的 PC (Program Counter) 指向的是當前的指令地址,經過 PC 的更新來運行咱們的程序,一個線程同一時刻只能執行一條指令。固然咱們知道線程和進程都是虛擬的概念,實際上 PC 是 CPU 核心中的寄存器,它是實際存在的,因此也能夠說一個 CPU 核心同一時刻只能執行一個線程。緩存
無論是多處理器設備仍是多核設備,開發者每每只須要關心 CPU 的核心數量,而不需關心它們的物理構成。CPU 核心數量是有限的,也就是說一個設備併發執行的線程數量是有限的,當線程數量超過 CPU 核心數量時,一個 CPU 核心每每就要處理多個線程,這個行爲叫作線程調度。安全
線程調度簡單來講就是:一個 CPU 核心輪流讓各個線程分別執行一段時間。固然這中間還包含着複雜的邏輯,後文再來分析。性能優化
在移動端開發中,由於系統的複雜性,開發者每每不能指望全部線程都能真正的併發執行,並且開發者也不清楚 XNU 什麼時候切換內核態線程、什麼時候進行線程調度,因此開發者要常常考慮到線程調度的狀況。多線程
當線程數量超過 CPU 核心數量,CPU 核心經過線程調度切換用戶態線程,意味着有上下文的轉換(寄存器數據、棧等),過多的上下文切換會帶來資源開銷。雖然內核態線程的切換理論上不會是性能負擔,開發中仍是應該儘可能減小線程的切換。併發
注意:使用 GCD 是操做隊列,隊列切換並不老是意味着線程的切換(GCD 會作好 CPU 親和性),代碼層面能夠減小隊列切換來優化。
看一段簡單的代碼:
dispatch_queue_t queue = dispatch_queue_create("x.x.x", DISPATCH_QUEUE_CONCURRENT); - (void)tast1 { dispatch_async(queue, ^{ //執行任務1 dispatch_async(dispatch_get_main_queue(), ^{ //任務1完成 [self tast2]; }); }); } - (void)tast2 { dispatch_async(queue, ^{ //執行任務2 dispatch_async(dispatch_get_main_queue(), ^{ //任務2完成 }); }); }
這裏建立了一個並行隊列,調用-tast1
會執行兩個任務,任務2要等待任務1執行完成,這裏一共有四次隊列的切換,明顯是多餘的,並且也不須要並行隊列來處理,優化以下:
dispatch_queue_t queue = dispatch_queue_create("x.x.x", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ //執行任務1 //執行任務2 dispatch_async(dispatch_get_main_queue(), ^{ //任務一、2完成 }); });
使用 GCD 並行隊列,當任務過多且耗時較長時,隊列會開闢大量的線程,而部分線程裏面的耗時任務已經耗盡了 CPU 資源,因此其餘的線程也只能等待 CPU 時間片,過多的線程也會讓線程調度過於頻繁。
GCD 中並行隊列並不能限制線程數量,能夠建立多個串行隊列來模擬並行的效果,業界知名框架 YYKit 就作了這個邏輯,經過和 CPU 核心數量相同的串行隊列輪詢返回來達到並行隊列的效果:
static dispatch_queue_t YYAsyncLayerGetDisplayQueue() { //最大隊列數量 #define MAX_QUEUE_COUNT 16 //隊列數量 static int queueCount; //使用棧區的數組存儲隊列 static dispatch_queue_t queues[MAX_QUEUE_COUNT]; static dispatch_once_t onceToken; static int32_t counter = 0; dispatch_once(&onceToken, ^{ //串行隊列數量和處理器數量相同 queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount; queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount; //建立串行隊列,設置優先級 if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) { for (NSUInteger i = 0; i < queueCount; i++) { dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0); queues[i] = dispatch_queue_create("com.ibireme.yykit.render", attr); } } else { for (NSUInteger i = 0; i < queueCount; i++) { queues[i] = dispatch_queue_create("com.ibireme.yykit.render", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(queues[i], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); } } }); //輪詢返回隊列 uint32_t cur = (uint32_t)OSAtomicIncrement32(&counter); return queues[cur % queueCount]; #undef MAX_QUEUE_COUNT }
然而這樣會致使串行隊列比較少,若你的任務不少時,會致使 CPU 資源利用率不高。YYKit 在異步繪製時使用這段代碼,這是一個任務不算多、耗時較長的場景,因此是比較適合的。
一般來講,線程調度除了輪轉法之外,還有優先級調度的方案,在線程調度時,高優先級的線程會更早的執行。有兩個概念須要明確:
特殊場景下,當多個 CPU 密集型線程霸佔了全部 CPU 資源,而它們的優先級都比較高,而此時優先級較低的 IO 密集型線程將持續等待,產生線程餓死的現象。固然,爲了不線程餓死,系統會逐步提升被「冷落」線程的優先級,IO 密集型線程一般狀況下比 CPU 密集型線程更容易獲取到優先級提高。
雖然系統會自動作這些事情,可是這總歸會形成時間等待,可能會影響用戶體驗。因此筆者認爲開發者須要從兩個方面權衡優先級問題:
好比一個場景:大量的圖片異步解壓的任務,解壓的圖片不須要當即反饋給用戶,同時又有大量的異步查詢磁盤緩存的任務,而查詢磁盤緩存任務完成事後須要反饋給用戶。
圖片解壓屬於 CPU 密集型線程,查詢磁盤緩存屬於 IO 密集型線程,然後者須要反饋給用戶更加緊急,因此應該讓圖片解壓線程的優先級低一點,查詢磁盤緩存的線程優先級高一點。
值得注意的是,這裏是說大量的異步任務,意味着 CPU 頗有可能滿負荷運算,若 CPU 資源綽綽有餘的狀況下就沒那個必要去處理優先級問題。
iOS 8 事後設置隊列優先級的方法以下:
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_BACKGROUND, 0); dispatch_queue_t queue = dispatch_queue_create("x.x.x", attr);
這裏就設置了一個QOS_CLASS_BACKGROUND
優先級,比較適合後臺異步下載大文件之類的業務。
有些業務只能寫在主線程,好比 UI 類組件的初始化及其佈局。其實這方面的優化就比較多了,業界所說的性能優化大部分都是爲了減輕主線程的壓力,彷佛有些偏離了多線程優化的範疇了,下面就基於主線程任務的管理大體羅列幾點吧:
經過內存複用來減小開闢內存的時間消耗,這在系統 UI 類組件中應用普遍,好比 UITableViewCell 的複用。同時,減小開闢內存意味着減小了內存釋放,一樣能節約 CPU 資源。
既然 UI 組件必須在主線程初始化,那麼就須要用時再初始化吧,swift 的寫時複製也是相似的思路。
經過監聽 Runloop 即將結束等通知,將大量的任務拆分開來,在每次 Runloop 循環週期執行少許任務。其實在實踐這種優化思路以前,應該想一想能不能將任務放到異步線程,而不是用這種比較極端的優化手段。
//這裏是主線程上下文 dispatch_async(dispatch_get_main_queue(), ^{ //等到主線程空閒執行該任務 });
這種手法挺巧,可讓 block 中的任務延遲到主線程空閒再執行,不過也不適合計算量過大的任務,由於始終是在主線程嘛。
多線程會帶來線程安全問題,當原子操做不能知足業務時,每每須要使用各類「鎖」來保證內存的讀寫安全。
經常使用的鎖有互斥鎖、讀寫鎖、空轉鎖,一般狀況下,iOS 開發中互斥鎖pthread_mutex_t、dispatch_semaphore_t
,讀寫鎖pthread_rwlock_t
就能知足大部分需求,而且性能不錯。
在讀取鎖失敗時,線程有可能有兩種狀態:
喚醒線程比較耗時,線程空轉須要消耗 CPU 資源而且時間越長消耗越多,由此可知空轉適合少許任務、掛起適合大量任務。
實際上互斥鎖和讀寫鎖都有空轉鎖的特性,它們在獲取鎖失敗時會先空轉一段時間,而後纔會掛起,而空轉鎖也不會永遠的空轉,在特定的空轉時間事後仍然會掛起,因此一般狀況下不用刻意去使用空轉鎖,Casa Taloyum 在博客中有詳細的解釋。
優先級反轉概念:好比兩個線程 A 和 B,優先級 A < B。當 A 獲取鎖訪問共享資源時,B 嘗試獲取鎖,那麼 B 就會進入忙等狀態,忙等時間越長對 CPU 資源的佔用越大;而因爲 A 的優先級低於 B,A 沒法與高優先級的線程爭奪 CPU 資源,從而致使任務遲遲完成不了。解決優先級反轉的方法有「優先級天花板」和「優先級繼承」,它們的核心操做都是提高當前正在訪問共享資源的線程的優先級。
OSSpinLock 因爲這個問題致使不少開源庫都放棄使用了,有興趣能夠看看一篇文章:再也不安全的 OSSpinLock。
很常見的場景是,同一線程重複獲取鎖致使的死鎖,這種狀況可使用遞歸鎖來處理,pthread_mutex_t
使用pthread_mutex_init_recursive()
方法初始化就能擁有遞歸鎖的特性。
使用pthread_mutex_trylock()
等嘗試獲取鎖的方法能有效的避免死鎖的狀況,在 YYCache 源碼中有一段處理就比較精緻:
while (!finish) { if (pthread_mutex_trylock(&_lock) == 0) { ... finish = YES; ... pthread_mutex_unlock(&_lock); } else { usleep(10 * 1000); //10 ms } }
這段代碼除了避免潛在的死鎖狀況外,還作了一個10ms的掛起操做而後循環嘗試,而不是直接讓線程空轉浪費過多的 CPU 資源。雖然掛起線程「浪費了」互斥鎖的空轉期,增長了喚醒線程的資源消耗,下降了鎖的性能,可是考慮到 YYCache 此處的業務是修剪內存,並不是是對鎖性能要求很高的業務,而且修剪的任務量可能比較大,出現線程競爭的概率較大,因此這裏放棄線程空轉直接掛起線程是一個不錯的處理方式。
開發者應該充分的理解業務,將臨界區儘可能縮小,不會出現線程安全問題的代碼就不要用鎖來保護了,這樣才能提升併發時鎖的性能。
當一個方法是可重入的時候,能夠放心大膽的使用,若一個方法不可重入,開發者應該多留意,思考這個方法會不會有多個線程訪問的狀況,如有就老老實實的加上線程鎖。
編譯器可能會爲了提升效率將變量寫入寄存器而暫時不寫回,方便下次使用,咱們知道一句代碼轉換爲指令不止一條,因此在變量寫入寄存器沒來得及寫回的過程當中,可能這個變量被其它線程讀寫了。編譯器一樣會爲了提升效率對它認爲順序無關的指令調換順序。
以上均可能會致使合理使用鎖的地方仍然線程不安全,而volatile
關鍵字就能夠解決這類問題,它能阻止編譯器爲了效率將變量緩存到寄存器而不及時寫回,也能阻止編譯器調整操做volatile
修飾變量的指令順序。
原子自增函數就有相似的應用:int32_t OSAtomicIncrement32( volatile int32_t *__theValue )
。
CPU 也可能爲了提升效率而去交換指令的順序,致使加鎖的代碼也不安全,解決這類問題可使用內存屏障,CPU 越過內存屏障後會刷新寄存器對變量的分配。
OC 實現單例模式的方法:
void _dispatch_once(dispatch_once_t *predicate, DISPATCH_NOESCAPE dispatch_block_t block) { if (DISPATCH_EXPECT(*predicate, ~0l) != ~0l) { dispatch_once(predicate, block); } else { dispatch_compiler_barrier(); } DISPATCH_COMPILER_CAN_ASSUME(*predicate == ~0l); }
其中就能看到內存屏障的宏:#define dispatch_compiler_barrier() __asm__ __volatile__("" ::: "memory")
;還有一個分支預測減小指令跳轉的優化宏(減小跳轉指令能提升 CPU 流水線執行的效率):#define DISPATCH_EXPECT(x, v) __builtin_expect((x), (v))
。
偏底層原理的東西比較抽象,筆者認爲搞清楚它爲何要這麼作比它作了什麼更爲重要,更能提高一我的的思惟。基礎技術每每在業務中的做用不是那麼大,可是卻能讓你更從容的編碼,超越普通開發者的思惟也能讓你在較複雜的業務中選擇更合理更高效的方案,你的代碼才能可靠。