標籤:
承接上一篇文章,今天我們來探討並髮網絡的線程管理。眾所周知在網路請求中,高並發的多線程網路請求非常普遍,我們不能因為上一條網路阻塞影響到其他的網路請求,然而過多的線程又會耗盡移動端上有限的CPU資源。如何處理多並行作業上,各家的網路架構多少都有些差異,今天我們就來看一看應該如何選擇。
隊列的選擇方案
網路請求一般都是採用FIFO的方式進行調度,所以採用隊列來儲存請求任務最合適不過了,在JAVA中比較常用的隊列有以下幾種
1.ArrayBlockingQueue
2.LinkedBlockingQueue
3.PriorityBlockingQueue
讓我們先來普及以下BlockingQueue的特點
BlockingQueue
多線程環境中,通過隊列可以很容易實現資料共用,比如經典的“生產者”和“消費者”模型中,通過隊列可以很便利地實現兩者之間的資料共用。假設我們有若干生產者線程,另外又有若干個消費者線程。如果生產者線程需要把準備好的資料共用給消費者線程,利用隊列的方式來傳遞資料,就可以很方便地解決他們之間的資料共用問題。但如果生產者和消費者在某個時間段內,萬一發生資料處理速度不匹配的情況呢?理想情況下,如果生產者產出資料的速度大於消費者消費的速度,並且當生產出來的資料累積到一定程度的時候,那麼生產者必須暫停等待一下(阻塞生產者線程),以便等待消費者線程把累積的資料處理完畢,反之亦然。然而,在concurrent包發布以前,在多線程環境下,我們每個程式員都必須去自己控制這些細節,尤其還要兼顧效率和安全執行緒,而這會給我們的程式帶來不小的複雜度。好在此時,強大的concurrent包橫空出世了,而他也給我們帶來了強大的BlockingQueue。(在多線程領域:所謂阻塞,在某些情況下會掛起線程(即阻塞),一旦條件滿足,被掛起的線程又會自動被喚醒)
接著我們來看看它的三個常用實現子類
ArrayBlockingQueue
基於數組的阻塞隊列實現,在ArrayBlockingQueue內部,維護了一個定長數組,以便緩衝隊列中的資料對象。ArrayBlockingQueue在生產者放入資料和消費者擷取資料,都是共用同一個鎖對象,由此也意味著兩者無法真正並行運行。
LinkedBlockingQueue
基於鏈表的阻塞隊列,同ArrayListBlockingQueue類似,在未指定長度的情況下,預設是最大值,這樣的話,如果生產者的速度一旦大於消費者的速度,也許還沒有等到隊列滿阻塞產生,系統記憶體就有可能已被消耗殆盡了。而LinkedBlockingQueue之所以能夠高效的處理並發資料,還因為其對於生產者端和消費者端分別採用了獨立的鎖來控制資料同步,這也意味著在高並發的情況下生產者和消費者可以並行地操作隊列中的資料,以此來提高整個隊列的並發效能。
PriorityBlockingQueue
基於優先順序的阻塞隊列(優先順序的判斷通過建構函式傳入的Compator對象來決定),但需要注意的是PriorityBlockingQueue並不會阻塞資料生產者,而只會在沒有可消費的資料時,阻塞資料的消費者。因此使用的時候要特別注意,生產者生產資料的速度絕對不能快於消費者消費資料的速度,否則時間一長,會最終耗盡所有的可用堆記憶體空間。在實現PriorityBlockingQueue時,內部控制線程同步的鎖採用的是公平鎖。
由上可知,傳統的隊列是線程阻塞的隊列,這就意味著當我們的網路端上生產者端或消費者端不平衡的時候,就很容易產生線程阻塞。舉個例子,如果使用者在短時間內進行了大量的網路操作,則消費者端的執行速度會遠遠的大於生產者端的速度,如果使用ArrayBlockingQueue的話,執行效率就會卡在同步鎖進行pull()和take()操作的上面,這樣的線程阻塞是不被接受的。這個時候LinkedBlockingQueue的優勢就遠遠顯示出來了,同一個元素在隊列中使用分離鎖的選擇可以讓LinkedBlockingQueue能夠高效的處理並發資料。
而優先順序隊列的使用則體現在另一個情境裡,假設現在使用者進入了一個首頁面需要擷取動態資料,同時後台提交了更新使用者狀態的操作,那麼如果更新狀態被阻塞的話,使用者的主介面資料就會遲遲重新整理不出來,這樣的使用者體驗就會變得很差。所以在進行網路請求的隊列選擇上,綜合考慮到移動端的並發效率和網路請求的優先順序,PriorityBlockingQueue和LinkedBlockingQueue的結合體才是最完美的解決方案。
多線程的選擇方案
在資源匱乏的移動端,效能最佳化是一個瓶頸。說到多線程並行作業,大家第一時間想到的一定是線程池。使用線程池可以減少在建立和銷毀線程上所花的時間以及系統資源的開銷 。固定數量的線程池可以有效防止記憶體資源被過度消耗殆盡,為UI線程爭取更多的資源。在Android的源碼中,已經有了最佳實務的模板,我也就不在這裡多贅述,上AsynckTask的源碼!
private static final String LOG_TAG = "AsyncTask"; private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private static final int CORE_POOL_SIZE = CPU_COUNT + 1; private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; private static final int KEEP_ALIVE = 1; private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread newThread(Runnable r) { return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); } }; private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(128); /** * An {@link Executor} that can be used to execute tasks in parallel. */ public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
高並行作業的情境下使用線程池來管理線程的確是不錯的實踐方案,並且我本人是這樣做的。但是在我們查看Volley的源碼時候會發現,Volley其實並沒有用到線程池,而是自己維護了一個長度為4的數組進行多線程的管理。
// Create network dispatchers (and corresponding threads) up to the pool size. for (int i = 0; i < mDispatchers.length; i++) { NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery); mDispatchers[i] = networkDispatcher; networkDispatcher.start(); }
我們可以看到,在進行網路請求分配的時候,從隊列裡取出的網路任務並沒有被交給線程池去分配資源,而是被交給一個初始化好的線程數組進行控制,那麼networkDispatcher裡面到底做了什麼呢?
public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); Request request; while (true) { try { // Take a request from the queue. request = mQueue.take(); } catch (InterruptedException e) { // We may have been interrupted because it was time to quit. if (mQuit) { return; } continue; } try { request.addMarker(network-queue-take); // If the request was cancelled already, do not perform the // network request. if (request.isCanceled()) { request.finish(network-discard-cancelled); continue; } // Tag the request (if API >= 14) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { TrafficStats.setThreadStatsTag(request.getTrafficStatsTag()); } // Perform the network request. NetworkResponse networkResponse = mNetwork.performRequest(request); request.addMarker(network-http-complete); // If the server returned 304 AND we delivered a response already, // we‘re done -- don‘t deliver a second identical response. if (networkResponse.notModified && request.hasHadResponseDelivered()) { request.finish(not-modified); continue; } // Parse the response here on the worker thread. Response<!--?--> response = request.parseNetworkResponse(networkResponse); request.addMarker(network-parse-complete); // Write to cache if applicable. // TODO: Only update cache metadata instead of entire record for 304s. if (request.shouldCache() && response.cacheEntry != null) { mCache.put(request.getCacheKey(), response.cacheEntry); request.addMarker(network-cache-written); } // Post the response back. request.markDelivered(); mDelivery.postResponse(request, response); } catch (VolleyError volleyError) { parseAndDeliverNetworkError(request, volleyError); } catch (Exception e) { VolleyLog.e(e, Unhandled exception %s, e.toString()); mDelivery.postError(request, new VolleyError(e)); } }}
在上面我們提到過,BlockingQueue 是一個線程阻塞的隊列,所以當隊列為空白時,消費者線程會一直阻塞等待網路任務的提交,所以在 while (true) {}中,request = mQueue.take();這段代碼就是一個線程阻塞的操作,mQuit用來控制線程結束釋放資源,如果網路請求沒有被Cancel,最後會執行mDelivery.postResponse(request, response)進行將結果回調傳遞到主線程。整個過程中我們都並沒有看見ThreadPoolExecutor的身影。為什麼要使用數組而不是線程池呢?
我們先來看一看線程池的調度規則ThreadPoolExecutor使用介紹
當一個任務通過execute(Runnable)方法欲添加到線程池時:
如果此時線程池中的數量小於corePoolSize,即使線程池中的線程都處於空閑狀態,也要建立新的線程來處理被添加的任務。
如果此時線程池中的數量等於 corePoolSize,但是緩衝隊列 workQueue未滿,那麼任務被放入緩衝隊列。
如果此時線程池中的數量大於corePoolSize,緩衝隊列workQueue滿,並且線程池中的數量小於maximumPoolSize,建新的線程來處理被添加的任務。
如果此時線程池中的數量大於corePoolSize,緩衝隊列workQueue滿,並且線程池中的數量等於maximumPoolSize,那麼通過handler所指定的策略來處理此任務。也就是:處理任務的優先順序為:核心線程corePoolSize、任務隊列workQueue、最大線程maximumPoolSize,如果三者都滿了,使用handler處理被拒絕的任務。
當線程池中的線程數量大於
corePoolSize時,如果某線程空閑時間超過keepAliveTime,線程將被終止。這樣,線程池可以動態調整池中的線程數。
線上程池裡,如果線程池中的數量小於corePoolSize,即使線程池中的線程都處於空閑狀態,也要建立新的線程來處理被添加的任務,這就意味著當corePoolSize未滿時,會出現進程資源被浪費的情況。相比於運行時申請線程資源,初始化時分配線程資源反而可以節省建立開支。所以這樣看來,Volley使用一個 mDispatchers[i] 來管理網路請求的多線程,也不乏是一種最佳化方案。
在這裡單開一篇文章講述了多線程的處理,總結一下在編寫網路架構的並發處理時我們需要考慮到的情況有以下三點:
1.網路請求的高並發線程執行效率
2.請求隊列的優先順序處理和取消隊列
3.多線程的資源分派與釋放
下一篇我將詳細描述如何使用httpUrlConnection進行要求標頭的封裝,資料報文的拼接,帶你瞭解RFC文檔下定義的幾種常見網路請求協議。
我的Android網路架構之旅(二)