OkHttp3原始碼分析[任務佇列]
OkHttp系列文章如下
本文目錄:
- 執行緒池基礎
- 反向代理模型
- OkHttp的任務排程
看過Wiki的都知道OkHttp擁有2種執行方式,一種是同步阻塞呼叫並直接返回的形式,另一種是通過內部執行緒池分發排程實現非阻塞的非同步回撥。本文主要分析第二種,即OkHttp在多併發網路下的分發排程過程。本文主要分析的是Dispatcher
物件
1. 執行緒池基礎
在初學Java的時候,各位可能會用new Thread + Handler
來寫非同步任務,它的坑網上已經爛大街了,比如不能自動關閉,迷之縮排難以維護,導致目前開發者幾乎不怎麼用它。而現在很多框架,比如Picasso,Rxjava等,都幫我們寫好了對應場景的執行緒池,但是執行緒池到底有什麼好呢?
1.1. 執行緒池好處都有啥
執行緒池的關鍵在於執行緒複用以減少非核心任務的損耗。下面內容是引用IBM知識庫中的例子:
多執行緒技術主要解決處理器單元內多個執行緒執行的問題,它可以顯著減少處理器單元的閒置時間,增加處理器單元的吞吐能力。但如果對多執行緒應用不當,會增加對單個任務的處理時間。可以舉一個簡單的例子:
假設在一臺伺服器完成一項任務的時間為T
T1 建立執行緒的時間 T2 線上程中執行任務的時間,包括執行緒間同步所需時間 T3 執行緒銷燬的時間
顯然T = T1+T2+T3。注意這是一個極度簡化的假設。
可以看出T1,T3是多執行緒本身的帶來的開銷(在Java中,通過對映pThead,並進一步通過SystemCall實現native執行緒),我們渴望減少T1,T3所用的時間,從而減少T的時間。但一些執行緒的使用者並沒有注意到這一點,所以在程式中頻繁的建立或銷燬執行緒,這導致T1和T3在T中佔有相當比例。顯然這是突出了執行緒的弱點(T1,T3),而不是優點(併發性)。
池技術正是關注如何縮短或調整T1,T3時間的技術,從而提高伺服器程式效能的。
- 通過對執行緒進行快取,減少了建立銷燬的時間損失
- 通過控制執行緒數量閥值,減少了當執行緒過少時帶來的CPU閒置(比如說長時間卡在I/O上了)與執行緒過多時對JVM的記憶體與執行緒切換時系統呼叫的壓力
類似的還有Socket連線池、DB連線池、CommonPool(比如Jedis)等技術。
在Java中,我們可以通過執行緒池工廠
或者自定義引數
來建立執行緒池,這些教程就不講了
1.2. OkHttp配置的執行緒池
在OkHttp,使用如下構造了單例執行緒池
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
引數說明如下:
- int corePoolSize: 最小併發執行緒數,這裡併發同時包括空閒與活動的執行緒,如果是0的話,空閒一段時間後所有執行緒將全部被銷燬。
- int maximumPoolSize: 最大執行緒數,當任務進來時可以擴充的執行緒最大值,當大於了這個值就會根據丟棄處理機制來處理
- long keepAliveTime: 當執行緒數大於
corePoolSize
時,多餘的空閒執行緒的最大存活時間,類似於HTTP中的Keep-alive - TimeUnit unit: 時間單位,一般用秒
- BlockingQueue<Runnable> workQueue: 工作佇列,先進先出,可以看出並不像Picasso那樣設定優先佇列。
- ThreadFactory threadFactory: 單個執行緒的工廠,可以打Log,設定
Daemon
(即當JVM退出時,執行緒自動結束)等
可以看出,在Okhttp中,構建了一個閥值為[0, Integer.MAX_VALUE]的執行緒池,它不保留任何最小執行緒數,隨時建立更多的執行緒數,當執行緒空閒時只能活60秒,它使用了一個不儲存元素的阻塞工作佇列,一個叫做"OkHttp Dispatcher"的執行緒工廠。
也就是說,在實際執行中,當收到10個併發請求時,執行緒池會建立十個執行緒,當工作完成後,執行緒池會在60s後相繼關閉所有執行緒。
在RxJava的
Schedulers.io()
中,也有類似的設計,只不過是執行緒池的池,最小的執行緒數量控制,不設上限的最大執行緒,以保證I/O任務中高阻塞低佔用的過程中,不會長時間卡在阻塞上,有興趣的可以分析RxJava中4種不同場景的Schedulers
反向代理模型
在OkHttp中,使用了與Nginx類似的反向代理與分發技術,這是典型的單生產者多消費者問題。
我們知道在Nginx/SLB中,使用者通過HTTP(Socket)訪問前置的伺服器,伺服器會新增Header並自動轉發請求給後端叢集,接著返回資料結果給使用者(比如簡書上次掛了也顯示了Nginx報錯)。通過將工作分配給多個後臺(無狀態的)伺服器並共享Session,可以提高服務的負載均衡能力,實現非阻塞、高可用、高併發連線,避免資源全部放到一臺伺服器而帶來的負載,速度,線上率等影響。
而在OkHttp中,非常類似於上述場景,它使用Dispatcher作為任務的派發器,執行緒池對應多臺後置伺服器,用AsyncCall
對應Socket請求,用Deque<readyAsyncCalls>
對應Nginx的內部快取
具體成員如下
- maxRequests = 64: 最大併發請求數為64
- maxRequestsPerHost = 5: 每個主機最大請求數為5
- Dispatcher: 分發者,也就是生產者(預設在主執行緒)
- AsyncCall: 佇列中需要處理的Runnable(包裝了非同步回撥介面)
- ExecutorService:消費者池(也就是執行緒池)
- Deque<readyAsyncCalls>:快取(用陣列實現,可自動擴容,無大小限制)
- Deque<runningAsyncCalls>:正在執行的任務,僅僅是用來引用正在執行的任務以判斷併發量,注意它並不是消費者快取
通過將請求任務分發給多個執行緒,可以顯著的減少I/O等待時間
OkHttp的任務排程
當我們希望使用OkHttp的非同步請求時,一般進行如下構造
OkHttpClient client = new OkHttpClient.Builder().build();
Request request = new Request.Builder()
.url("http://qq.com").get().build();
client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Call call, IOException e) {
}
@Override public void onResponse(Call call, Response response) throws IOException {
}
});
當HttpClient的請求入隊時,根據程式碼,我們可以發現實際上是Dispatcher進行了入隊操作
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
//新增正在執行的請求
runningAsyncCalls.add(call);
//執行緒池執行請求
executorService().execute(call);
} else {
//新增到快取佇列排隊等待
readyAsyncCalls.add(call);
}
}
可以發現請求是否進入快取的條件如下:
(runningRequests<64 && runningRequestsPerHost<5)
如果滿足條件,那麼就直接把AsyncCall
直接加到runningCalls
的佇列中,並線上程池中執行(執行緒池會根據當前負載自動建立,銷燬,快取相應的執行緒)。反之就放入readyAsyncCalls
進行快取等待。
我們再分析請求元素AsyncCall(它實現了Runnable介面),它內部實現的execute方法如下
@Override protected void execute() {
boolean signalledCallback = false;
try {
//執行耗時IO任務
Response response = getResponseWithInterceptorChain(forWebSocket);
if (canceled) {
signalledCallback = true;
//回撥,注意這裡回撥是線上程池中,而不是想當然的主執行緒回撥
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
//回撥,同上
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
} else {
responseCallback.onFailure(RealCall.this, e);
}
} finally {
//最關鍵的程式碼
client.dispatcher().finished(this);
}
}
當任務執行完成後,無論是否有異常,finally
程式碼段總會被執行,也就是會呼叫Dispatcher的finished函式,開啟原始碼,發現它將正在執行的任務Call從佇列runningAsyncCalls中移除後,接著執行promoteCalls()
函式
private void promoteCalls() {
//如果目前是最大負荷運轉,接著等
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
//如果快取等待區是空的,接著等
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
if (runningCallsForHost(call) < maxRequestsPerHost) {
//將快取等待區最後一個移動到執行區中,並執行
i.remove();
runningAsyncCalls.add(call);
executorService().execute(call);
}
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}
這樣,就主動的把快取佇列向前走了一步,而沒有使用互斥鎖等複雜編碼
Summary
通過上述的分析,我們知道了:
- OkHttp採用Dispatcher技術,類似於Nginx,與執行緒池配合實現了高併發,低阻塞的執行
- Okhttp採用Deque作為快取,按照入隊的順序先進先出
- OkHttp最出彩的地方就是在try/finally中呼叫了
finished
函式,可以主動控制等待佇列的移動,而不是採用鎖或者wait/notify,極大減少了編碼複雜性
Refference
相關文章
- OkHttp 3.7原始碼分析(三)——任務佇列HTTP原始碼佇列
- OkHttp3.7原始碼分析(三)——任務佇列HTTP原始碼佇列
- [原始碼分析] 分散式任務佇列 Celery 之 傳送Task & AMQP原始碼分散式佇列MQ
- [原始碼分析] 並行分散式任務佇列 Celery 之 Timer & Heartbeat原始碼並行分散式佇列
- Node.js 任務佇列Bull的原始碼淺析Node.js佇列原始碼
- Celery任務佇列佇列
- [原始碼分析] 分散式任務佇列 Celery 多執行緒模型 之 子程式原始碼分散式佇列執行緒模型
- 任務佇列,巨集任務與微任務佇列
- laravel原始碼分析-佇列QueueLaravel原始碼佇列
- [原始碼分析]並行分散式任務佇列 Celery 之 子程式處理訊息原始碼並行分散式佇列
- OkHttp3原始碼分析[DiskLruCache]HTTP原始碼
- [原始碼解析] 並行分散式任務佇列 Celery 之 多程式模型原始碼並行分散式佇列模型
- [原始碼解析] 分散式任務佇列 Celery 之啟動 Consumer原始碼分散式佇列
- [原始碼解析] 並行分散式任務佇列 Celery 之 負載均衡原始碼並行分散式佇列負載
- OkHttp3原始碼分析[綜述]HTTP原始碼
- js定時任務佇列JS佇列
- 原始碼|jdk原始碼之棧、佇列及ArrayDeque分析原始碼JDK佇列
- [原始碼解析] 並行分散式任務佇列 Celery 之 Task是什麼原始碼並行分散式佇列
- Redis實現任務佇列、優先順序佇列Redis佇列
- okhttp3 攔截器原始碼分析HTTP原始碼
- OkHttp3原始碼分析[快取策略]HTTP原始碼快取
- 事件迴圈與任務佇列事件佇列
- 如何寫一個任務佇列佇列
- javascript事件環微任務和巨集任務佇列原理JavaScript事件佇列
- 詳解JavaScript的任務、微任務、佇列以及程式碼執行順序JavaScript佇列
- [原始碼分析] 訊息佇列 Kombu 之 Hub原始碼佇列
- [原始碼解析] 並行分散式任務佇列 Celery 之 消費動態流程原始碼並行分散式佇列
- LiteOS-任務篇-原始碼分析-任務排程函式原始碼函式
- 【OkHttp3原始碼分析】(一)Request的executeHTTP原始碼
- 【OkHttp3原始碼分析】(二)Request的enqueueHTTP原始碼ENQ
- 簡單易用的任務佇列-beanstalkd佇列Bean
- iOS-佇列.執行緒.任務iOS佇列執行緒
- (轉)開源的任務佇列服務HTQ佇列
- Laravel 佇列原始碼解析Laravel佇列原始碼
- MapReduce——客戶端提交任務原始碼分析客戶端原始碼
- LiteOS核心原始碼分析:任務LOS_Schedule原始碼
- quartz2.3任務管理的原始碼分析quartz原始碼
- Spark on Yarn 任務提交流程原始碼分析SparkYarn原始碼