OkHttp 3.7原始碼分析(三)——任務佇列
前面的部落格已經提到過,OkHttp的一個高效之處在於在內部維護了一個執行緒池,方便高效地執行非同步請求。本篇部落格將詳細介紹OkHttp的任務佇列機制。
1. 執行緒池的優點
OkHttp的任務佇列在內部維護了一個執行緒池用於執行具體的網路請求。而執行緒池最大的好處在於通過執行緒複用減少非核心任務的損耗。
多執行緒技術主要解決處理器單元內多個執行緒執行的問題,它可以顯著減少處理器單元的閒置時間,增加處理器單元的吞吐能力。但如果對多執行緒應用不當,會增加對單個任務的處理時間。可以舉一個簡單的例子:
假設在一臺伺服器完成一項任務的時間為T
T1 建立執行緒的時間 T2 線上程中執行任務的時間,包括執行緒間同步所需時間 T3 執行緒銷燬的時間
- 1
- 2
- 3
- 1
- 2
- 3
顯然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)等技術。
2. OkHttp的任務佇列
OkHttp的任務佇列主要由兩部分組成:
- 任務分發器dispatcher:負責為任務找到合適的執行執行緒
- 網路請求任務執行緒池
public final class Dispatcher {
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
private Runnable idleCallback;
/** Executes calls. Created lazily. */
private ExecutorService executorService;
/** Ready async calls in the order they'll be run. */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
public Dispatcher(ExecutorService executorService) {
this.executorService = executorService;
}
public Dispatcher() {
}
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;
}
...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
引數說明如下:
- readyAsyncCalls:待執行非同步任務佇列
- runningAsyncCalls:執行中非同步任務佇列
- runningSyncCalls:執行中同步任務佇列
- executorService:任務佇列執行緒池:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- int corePoolSize: 最小併發執行緒數,這裡併發同時包括空閒與活動的執行緒,如果是0的話,空閒一段時間後所有執行緒將全部被銷燬
- int maximumPoolSize: 最大執行緒數,當任務進來時可以擴充的執行緒最大值,當大於了這個值就會根據丟棄處理機制來處理
- long keepAliveTime: 當執行緒數大於
corePoolSize
時,多餘的空閒執行緒的最大存活時間,類似於HTTP中的Keep-alive- TimeUnit unit: 時間單位,一般用秒
- BlockingQueue workQueue: 工作佇列,先進先出,可以看出並不像Picasso那樣設定優先佇列
- ThreadFactory threadFactory: 單個執行緒的工廠,可以打Log,設定
Daemon
(即當JVM退出時,執行緒自動結束)等可以看出,在Okhttp中,構建了一個閥值為[0, Integer.MAX_VALUE]的執行緒池,它不保留任何最小執行緒數,隨時建立更多的執行緒數,當執行緒空閒時只能活60秒,它使用了一個不儲存元素的阻塞工作佇列,一個叫做”OkHttp Dispatcher”的執行緒工廠。
也就是說,在實際執行中,當收到10個併發請求時,執行緒池會建立十個執行緒,當工作完成後,執行緒池會在60s後相繼關閉所有執行緒。
3. Dispatcher分發器
dispatcher分發器類似於Ngnix中的反向代理,通過Dispatcher將任務分發到合適的空閒執行緒,實現非阻塞,高可用,高併發連線
1.同步請求
當我們使用OkHttp進行同步請求時,一般構造如下:
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
Response response = client.newCall(request).execute();
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
接下來看看RealCall.execute
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} finally {
client.dispatcher().finished(this);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
同步呼叫的執行邏輯是:
- 將對應任務加入分發器
- 執行任務
- 執行完成後通知dispatcher對應任務已完成,對應任務出隊
2.非同步請求
非同步請求一般構造如下:
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.d("OkHttp", "Call Failed:" + e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.d("OkHttp", "Call succeeded:" + response.message());
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
當HttpClient的請求入隊時,根據程式碼,我們可以發現實際上是Dispatcher進行了入隊操作。
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
//新增正在執行的請求
runningAsyncCalls.add(call);
//執行緒池執行請求
executorService().execute(call);
} else {
//新增到快取佇列排隊等待
readyAsyncCalls.add(call);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
如果滿足條件:
- 當前請求數小於最大請求數(64)
- 對單一host的請求小於閾值(5)
將該任務插入正在執行任務佇列,並執行對應任務。如果不滿足則將其放入待執行佇列。
接下來看看AsyncCall.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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
當任務執行完成後,無論成功與否都會呼叫dispatcher.finished方法,通知分發器相關任務已結束:
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
if (promoteCalls) promoteCalls();
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 空閒出多餘執行緒,呼叫promoteCalls呼叫待執行的任務
- 如果當前整個執行緒池都空閒下來,執行空閒通知回撥執行緒(idleCallback)
接下來看看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.
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
promoteCalls的邏輯也很簡單:掃描待執行任務佇列,將任務放入正在執行任務佇列,並執行該任務。
4. 總結
以上就是整個任務佇列的實現細節,總結起來有以下幾個特點:
- OkHttp採用Dispatcher技術,類似於Nginx,與執行緒池配合實現了高併發,低阻塞的執行
- Okhttp採用Deque作為快取,按照入隊的順序先進先出
- OkHttp最出彩的地方就是在try/finally中呼叫了
finished
函式,可以主動控制等待佇列的移動,而不是採用鎖或者wait/notify,極大減少了編碼複雜性
相關文章
- OkHttp3.7原始碼分析(三)——任務佇列HTTP原始碼佇列
- OkHttp3原始碼分析[任務佇列]HTTP原始碼佇列
- OkHttp 3.7原始碼分析(四)——快取策略HTTP原始碼快取
- OkHttp 3.7原始碼分析(五)——連線池HTTP原始碼
- OkHttp 3.7原始碼分析(一)——整體架構HTTP原始碼架構
- OkHttp3.7原始碼分析(四)——快取策略HTTP原始碼快取
- [原始碼分析] 分散式任務佇列 Celery 之 傳送Task & AMQP原始碼分散式佇列MQ
- 原始碼分析三:OkHttp—CacheInterceptor原始碼HTTP
- 原始碼分析三:OkHttp—CallServerInterceptor原始碼HTTPServer
- 原始碼分析三:OkHttp—RetryAndFollowUpInterceptor原始碼HTTP
- 原始碼分析三:OkHttp—BridgeInterceptor原始碼HTTP
- 原始碼分析三:OkHttp—ConnectInterceptor原始碼HTTP
- [原始碼分析] 並行分散式任務佇列 Celery 之 Timer & Heartbeat原始碼並行分散式佇列
- Node.js 任務佇列Bull的原始碼淺析Node.js佇列原始碼
- Celery任務佇列佇列
- [原始碼分析] 分散式任務佇列 Celery 多執行緒模型 之 子程式原始碼分散式佇列執行緒模型
- 任務佇列,巨集任務與微任務佇列
- laravel原始碼分析-佇列QueueLaravel原始碼佇列
- React原始碼解析(三):詳解事務與更新佇列React原始碼佇列
- OkHttp原始碼分析HTTP原始碼
- [原始碼分析]並行分散式任務佇列 Celery 之 子程式處理訊息原始碼並行分散式佇列
- 原始碼分析三:OkHttp(1)—總體架構原始碼HTTP架構
- [原始碼解析] 並行分散式任務佇列 Celery 之 多程式模型原始碼並行分散式佇列模型
- [原始碼解析] 分散式任務佇列 Celery 之啟動 Consumer原始碼分散式佇列
- [原始碼解析] 並行分散式任務佇列 Celery 之 負載均衡原始碼並行分散式佇列負載
- RabbitMQ訊息佇列(三):任務分發機制MQ佇列
- js定時任務佇列JS佇列
- 原始碼|jdk原始碼之棧、佇列及ArrayDeque分析原始碼JDK佇列
- gulp原始碼解析(三)—— 任務管理原始碼
- 原始碼分析三:OkHttp(2)—攔截器簡介原始碼HTTP
- [原始碼解析] 並行分散式任務佇列 Celery 之 Task是什麼原始碼並行分散式佇列
- Redis實現任務佇列、優先順序佇列Redis佇列
- OkHttp3.0-原始碼分析HTTP原始碼
- 原始碼分析筆記——OkHttp原始碼筆記HTTP
- 事件迴圈與任務佇列事件佇列
- 如何寫一個任務佇列佇列
- javascript事件環微任務和巨集任務佇列原理JavaScript事件佇列
- 詳解JavaScript的任務、微任務、佇列以及程式碼執行順序JavaScript佇列