Android中多執行緒在程式中的使用
什麼是多執行緒?
* 我們可以形象的把多執行緒的執行,形容為:在互相搶CPU的資源,這就是多執行緒的特性:隨機性。
* 就是誰搶到,誰執行,至於執行多長時間,CPU說了算。
為什麼要使用多執行緒?
這麼解釋問題吧:
1。單程序單執行緒:一個人在一個桌子上吃菜。
2。單程序多執行緒:多個人在同一個桌子上一起吃菜。
3。多程序單執行緒:多個人每個人在自己的桌子上吃菜。
優點:
1. 多執行緒可以提高使用者的體驗,讓系統執行的更快更流暢。
2. 可以發揮多核處理的優勢,提升CPU的使用率。
3. 將耗時較長的任務(如網路請求、下載檔案、資料庫訪問)放到子執行緒中執行,防止子執行緒卡死。
缺點:
1. 每開闢一個執行緒就消耗一定的資源。
2. 存線上程安全問題。
3. 如果出現多個執行緒同時訪問一個檔案,會出現資源爭奪的情況。
多執行緒的使用場景
- AsyncTask: 為 UI 執行緒與工作執行緒之間進行快速的切換提供一種簡單便捷的機制。適用於當下立即需要啟動,但是非同步執行的生命週期短暫的使用場景。
- HandlerThread: 為某些回撥方法或者等待某些任務的執行設定一個專屬的執行緒,並提供執行緒任務的排程機制。
- ThreadPool: 把任務分解成不同的單元,分發到各個不同的執行緒上,進行同時併發處理。
- IntentService: 適合於執行由 UI 觸發的後臺 Service 任務,並可以把後臺任務執行的情況通過一定的機制反饋給 UI。
瞭解這些系統提供的多執行緒工具類分別適合在什麼場景下,可以幫助我們選擇合適的解決方案,避免出現不可預期的麻煩。雖然使用多執行緒可以提高程式的併發量,但是我們需要特別注意因為引入多執行緒而可能伴隨而來的記憶體問題。舉個例子,在 Activity 內部定義的一個 AsyncTask,它屬於一個內部類,該類本身和外面的 Activity 是有引用關係的,如果 Activity 要銷燬的時候,AsyncTask 還仍然在執行,這會導致 Activity 沒有辦法完全釋放,從而引發記憶體洩漏。所以說,多執行緒是提升程式效能的有效手段之一,但是使用多執行緒卻需要十分謹慎小心,如果不瞭解背後的執行機制以及使用的注意事項,很可能引起嚴重的問題。
首先,預設情況下,所有的 AsyncTask 任務都是被線性排程執行的,他們處在同一個任務隊列當中,按順序逐個執行。假設你按照順序啟動20個 AsyncTask,一旦其中的某個 AsyncTask 執行時間過長,佇列中的其他剩餘 AsyncTask 都處於阻塞狀態,必須等到該任務執行完畢之後才能夠有機會執行下一個任務。情況如下圖所示:
為了解決上面提到的線性佇列等待的問題,我們可以使用 AsyncTask.executeOnExecutor()
強制指定 AsyncTask 使用執行緒池併發排程任務。
其次,如何才能夠真正的取消一個 AsyncTask 的執行呢?我們知道 AsyncTaks 有提供
cancel()
的方法,但是這個方法實際上做了什麼事情呢?執行緒本身並不具備中止正在執行的程式碼的能力,為了能夠讓一個執行緒更早的被銷燬,我們需要在 doInBackground()
的程式碼中不斷的新增程式是否被中止的判斷邏輯,如下圖所示:一旦任務被成功中止,AsyncTask 就不會繼續呼叫
onPostExecute()
,而是通過呼叫 onCancelled()
的回撥方法反饋任務執行取消的結果。我們可以根據任務回撥到哪個方法(是 onPostExecute 還是 onCancelled)來決定是對 UI 進行正常的更新還是把對應的任務所佔用的記憶體進行銷燬等。最後,使用 AsyncTask 很容易導致記憶體洩漏,一旦把 AsyncTask 寫成 Activity 的內部類的形式就很容易因為 AsyncTask 生命週期的不確定而導致 Activity 發生洩漏。
綜上所述,AsyncTask 雖然提供了一種簡單便捷的非同步機制,但是我們還是很有必要特別關注到他的缺點,避免出現因為使用錯誤而導致的嚴重系統性能問題。
HandlerThread
大多數情況下,AsyncTask 都能夠滿足多執行緒併發的場景需要(在工作執行緒執行任務並返回結果到主執行緒),但是它並不是萬能的。例如開啟相機之後的預覽幀資料是通過onPreviewFrame()
的方法進行回撥的,onPreviewFrame()
和 open()
相機的方法是執行在同一個執行緒的。如果這個回撥方法執行在 UI 執行緒,那麼在 onPreviewFrame()裡面將要執行的資料轉換操作將和主執行緒的介面繪製,事件傳遞等操作爭搶系統資源,這就有可能影響到主介面的表現效能。
我們需要確保 onPreviewFrame()執行在工作執行緒。如果使用 AsyncTask,會因為 AsyncTask 預設的線性執行的特性(即使換成併發執行)會導致因為無法把任務及時傳遞給工作執行緒而導致任務在主執行緒中被延遲,直到工作執行緒空閒,才可以把任務切換到工作執行緒中進行執行。
翻譯上面的話:其實是這樣的,當我們的需求是連續線性的將非同步執行結果傳遞給主執行緒時,由於AsyncTask預設的線性執行特性(也就是當多個AsyncTask任務執行時,需要等待當前任務執行完畢,才可以執行下一個任務),無法及時將非同步處理結果連續的傳遞給主執行緒,導致介面的顯示延遲,造成卡頓的現象。
所以我們需要的是一個執行在工作執行緒,同時又能夠處理佇列中的複雜任務的功能,而 HandlerThread 的出現就是為了實現這個功能的,它組合了 Handler,MessageQueue,Looper 實現了一個長時間執行的執行緒,不斷的從佇列中獲取任務進行執行的功能。
翻譯:也就是當多個非同步任務執行時,採用HandlerThread會將執行結果新增至MQ訊息佇列中,而主執行緒會從訊息佇列中不斷的輪詢是否有新的訊息任務。
回到剛才的處理相機回撥資料的例子,使用 HandlerThread 我們可以把 open()操作與 onPreviewFrame()的操作執行在同一個執行緒,同時還避免了 AsyncTask 的弊端。如果需要在 onPreviewFrame()裡面更新 UI,只需要呼叫 runOnUiThread()方法把任務回撥給主執行緒就夠了。
HandlerThread 比較合適處理那些在工作執行緒執行,需要花費時間偏長的任務。我們只需要把任務傳送給 HandlerThread,然後就只需要等待任務執行結束的時候通知返回到主執行緒就好了。
另外很重要的一點是,一旦我們使用了 HandlerThread,需要特別注意給 HandlerThread 設定不同的執行緒優先順序,CPU 會根據設定的不同執行緒優先順序對所有的執行緒進行排程優化。
掌握 HandlerThread 與 AsyncTask 之間的優缺點,可以幫助我們選擇合適的方案。
Swimming in Threadpools
執行緒池適合用在把任務進行分解,併發進行執行的場景。通常來說,系統裡面會針對不同的任務設定一個單獨的守護執行緒用來專門處理這項任務。例如使用 Networking Thread 用來專門處理網路請求的操作,使用 IO Thread 用來專門處理系統的 I\O 操作。針對那些場景,這樣設計是沒有問題的,因為對應的任務單次執行的時間並不長而且可以是順序執行的。但是這種專屬的單執行緒並不能滿足所有的情況,例如我們需要一次性 decode 40張圖片,每個執行緒需要執行 4ms 的時間,如果我們使用專屬單執行緒的方案,所有圖片執行完畢會需要花費 160ms(40*4),但是如果我們建立10個執行緒,每個執行緒執行4個任務,那麼我們就只需要16ms就能夠把所有的圖片處理完畢。為了能夠實現上面的執行緒池模型,系統為我們提供了
ThreadPoolExecutor
幫助類來簡化實現,剩下需要做的就只是對任務進行分解就好了。使用執行緒池需要特別注意同時併發執行緒數量的控制,理論上來說,我們可以設定任意你想要的併發數量,但是這樣做非常的不好。因為 CPU 只能同時執行固定數量的執行緒數,一旦同時併發的執行緒數量超過 CPU 能夠同時執行的閾值,CPU 就需要花費精力來判斷到底哪些執行緒的優先順序比較高,需要在不同的執行緒之間進行排程切換。
一旦同時併發的執行緒數量達到一定的量級,這個時候 CPU 在不同執行緒之間進行排程的時間就可能過長,反而導致效能嚴重下降。另外需要關注的一點是,每開一個新的執行緒,都會耗費至少 64K+ 的記憶體。為了能夠方便的對執行緒數量進行控制,ThreadPoolExecutor 為我們提供了初始化的併發執行緒數量,以及最大的併發數量進行設定。
另外需要關注的一個問題是:Runtime.getRuntime().availableProcesser()
方法並不可靠,他返回的值並不是真實的 CPU 核心數,因為 CPU 會在某些情況下選擇對部分核心進行睡眠處理,在這種情況下,返回的數量就只能是啟用的 CPU 核心數。
The Zen of IntentService
預設的 Service 是執行在主執行緒的,可是通常情況下,這很容易影響到程式的繪製效能(搶佔了主執行緒的資源)。除了前面介紹過的 AsyncTask 與 HandlerThread,我們還可以選擇使用 IntentService 來實現非同步操作。IntentService 繼承自普通 Service 同時又在內部建立了一個 HandlerThread,在onHandlerIntent()
的回撥裡面處理扔到 IntentService 的任務。所以 IntentService 就不僅僅具備了非同步執行緒的特性,還同時保留了 Service 不受主頁面生命週期影響的特點。
如此一來,我們可以在 IntentService 裡面通過設定鬧鐘間隔性的觸發非同步任務,例如重新整理資料,更新快取的圖片或者是分析使用者操作行為等等,當然處理這些任務需要小心謹慎。
使用 IntentService 需要特別留意以下幾點:- 首先,因為 IntentService 內建的是 HandlerThread 作為非同步執行緒,所以每一個交給 IntentService 的任務都將以佇列的方式逐個被執行到,一旦佇列中有某個任務執行時間過長,那麼就會導致後續的任務都會被延遲處理。
- 其次,通常使用到 IntentService 的時候,我們會結合使用 BroadcastReceiver 把工作執行緒的任務執行結果返回給主 UI 執行緒。使用廣播容易引起效能問題,我們可以使用 LocalBroadcastManager 來發送只在程式內部傳遞的廣播,從而提升廣播的效能。我們也可以使用
runOnUiThread()
快速回調到主 UI 執行緒。 - 最後,包含正在執行的 IntentService 的程式相比起純粹的後臺程式更不容易被系統殺死,該程式的優先順序是介於前臺程式與純後臺程式之間的。
The Importance of Thread Priority
理論上來說,我們的程式可以創建出非常多的子執行緒一起併發執行的,可是基於 CPU 時間片輪轉排程的機制,不可能所有的執行緒都可以同時被排程執行,CPU 需要根據執行緒的優先順序賦予不同的時間片。Android 系統會根據當前執行的可見的程式和不可見的後臺程式對執行緒進行歸類,劃分為 forground 的那部分執行緒會大致佔用掉 CPU 的90%左右的時間片,background 的那部分執行緒就總共只能分享到5%-10%左右的時間片。之所以設計成這樣是因為 forground 的程式本身的優先順序就更高,理應得到更多的執行時間。
預設情況下,新建立的執行緒的優先順序預設和建立它的母執行緒保持一致。如果主 UI 執行緒創建出了幾十個工作執行緒,這些工作執行緒的優先順序就預設和主執行緒保持一致了,為了不讓新建立的工作執行緒和主執行緒搶佔 CPU 資源,需要把這些執行緒的優先順序進行降低處理,這樣才能給幫組 CPU 識別主次,提高主執行緒所能得到的系統資源。
在 Android 系統裡面,我們可以通過
android.os.Process.setThreadPriority(int)
設定執行緒的優先順序,引數範圍從-20到19,數值越小優先順序越高。Android 系統還為我們提供了以下的一些預設值,我們可以通過給不同的工作執行緒設定不同數值的優先順序來達到更細粒度的控制。大多數情況下,新建立的執行緒優先順序會被設定為預設的0,主執行緒設定為0的時候,新建立的執行緒還可以利用
THREAD_PRIORITY_LESS_FAVORABLE
或者 THREAD_PRIORITY_MORE_FAVORABLE
來控制執行緒的優先順序。Android 系統裡面的 AsyncTask 與 IntentService已經預設幫助我們設定執行緒的優先順序,但是對於那些非官方提供的多執行緒工具類,我們需要特別留意根據需要自己手動來設定執行緒的優先順序。