OkHttp3.0解析——談談內部任務分發器dispatcher

晨雨細曲發表於2018-11-19

前言

OkHttp之所以能夠高效處理任務的一個很重要原因在於其內部維護了三個任務佇列(readyAsyncCalls、runningAsyncCalls、runningSyncCalls)和一個執行緒池(ThreadPoolExecutor)。這四個東西由內部的任務分發器dispathcer來進行調控處理,從而達到高效處理多工的效果。

執行緒池的作用不言而喻,他的主要作用在於可以避免我們在使用執行緒進行耗時任務的時候每次都開啟執行緒,用完之後又銷燬執行緒所帶來的效率與效能問題。他可以對執行緒進行多次的操作並複用空閒執行緒,從而達到不需要每次都開啟以及銷燬執行緒的目的。關於執行緒的知識,如果有不瞭解的可以去參考我寫的這篇文章 Java中的執行緒詳解,裡面對執行緒池的各種型別還有內部操作有詳盡的介紹。

OkHttp的任務佇列

okHttp中的任務佇列由兩部分組成:

  • 任務分發器dispatcher:負責幫助需要執行任務找到合適的任務佇列
  • 執行緒池ThreadPoolExecutor,用於執行dispatcher分配的任務 來看下任務排程器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;
  }
  
  ...
}
 

複製程式碼

可以看到,dispatcher裡面例項化了三個任務佇列readyAsyncCalls、runningAsyncCalls與runningSyncCalls還有一個執行緒池ThreadPoolExecutor。

readyAsyncCalls:等待執行非同步任務佇列。當有任務需要dispatcher將其新增進入執行緒池時,會先判斷執行緒池是否還有可以執行的執行緒,如果發現沒有執行的執行緒,此時先將任務放入到這個任務佇列中等待,等到執行緒池有空閒執行緒可以執行任務的時候再從這個任務佇列中取出任務交給執行緒池去處理。

runningAsyncCalls:執行中非同步任務佇列。儲存dispatcher將任務交給執行緒池去處理的任務。

runningSyncCalls:執行中的同步佇列。同步佇列和非同步佇列不同,他是一個序列的,而不是並行的,所以這個代表在同步操作的情況下執行的佇列。

我們看到在executeService()方法中建立了一個執行緒池ThreadPoolExecutor,裡面第一個引數核心執行緒數設定為了0,代表在空閒一段時間後執行緒將會被全部銷燬。

可以看出,在Okhttp中,構建了一個閥值為[0, Integer.MAX_VALUE]的執行緒池,它不保留任何最小執行緒數,隨時建立更多的執行緒數,當執行緒空閒時只能活60秒,它使用了一個不儲存元素的阻塞工作佇列,一個叫做"OkHttp Dispatcher"的執行緒工廠。

也就是說,在實際執行中,當收到10個併發請求時,執行緒池會建立十個執行緒,當工作完成後,執行緒池會在60s後相繼關閉所有執行緒。

Dispatcher分發器

dispatcher分發器類似於Ngnix中的反向代理,通過Dispatcher將任務分發到合適的空閒執行緒,實現非阻塞,高可用,高併發連線。

OkHttp3.0解析——談談內部任務分發器dispatcher

同步請求

 @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. 判斷任務是否正在執行executed,如果正在執行則丟擲異常。這代表同一個任務一次只能被執行一次,而並不能被執行多次。

  2. 將任務交給任務呼叫器,dispatcher呼叫executed去執行這個任務。

  3. 通過getResponseWithInterceptorChain()鏈呼叫攔截器,之後將任務執行的結果返回Response。

4.之後在任務執行完成呼叫dispatcher將其finish掉。

至此一個同步請求任務就算完成了。這裡關於getResponseWithInterceptorChain()中執行的一些攔截器的操作,以後我會專門寫一篇文章來講解OkHttp的攔截器的原理。

非同步操作

synchronized void enqueue(AsyncCall call) {
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      //新增正在執行的請求
    runningAsyncCalls.add(call);
       //執行緒池執行請求
    executorService().execute(call);
  } else {
      //新增到快取佇列排隊等待
    readyAsyncCalls.add(call);
  }
}

複製程式碼

在非同步操作中,會先去判斷runningAsyncCalls佇列中的任務數量是否會大於最大請求數量(maxRequest),這個的最大請求數量為64,然後在判斷是否runningCallsForHost是否小於maxRequestsPerHost(單一host請求)。如果兩個當中有一個不滿足,則代表執行緒池中可執行的執行緒數不夠,不能將任務新增到執行緒中去執行。此時則將任務直接新增到快取佇列排隊等待(readyAsyncCalls),等到有可執行的執行緒的時候再將任務新增到正在執行的佇列中,再呼叫執行緒池去執行call任務。

接下來看看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);
  }
}


複製程式碼

可以看到裡面有呼叫了攔截器鏈getResponseWithInterceptorChain(),並將任務的結果又一次返回Response。裡面會根據任務是否被Cancled而去回撥不同的方法。被Canceled就去呼叫onFailure(0方法,在裡面處理失敗的邏輯,成功就去呼叫成功的方法Response(),並將返回值交給他去處理。最後無論成功還是失敗都會去呼叫dispatcher的finish方法來結束掉這個任務。

我們在來深入看下finish的方法裡面做了哪些操作:

 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();
    }
  }


複製程式碼
  • 空閒出多餘執行緒,呼叫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.
    }
  }


複製程式碼

promoteCalls的邏輯也很簡單:掃描待執行任務佇列,將任務放入正在執行任務佇列,並執行該任務。

總結

以上就是整個任務佇列的實現細節,總結起來有以下幾個特點:

  1. OkHttp採用Dispatcher技術,類似於Nginx,與執行緒池配合實現了高併發、地阻塞的操作。
  2. OkHttp採用佇列進行快取,按照入列的特點先進先出來執行任務
  3. OkHttp最出彩的地方就是在try/finally中呼叫了finish函式,可以主動控制等待佇列的移動,而不是採用鎖或者wait/notify,極大的減少了編碼的複雜性。

有興趣可以關注我的小專欄,學習更多知識:小專欄

OkHttp3.0解析——談談內部任務分發器dispatcher

相關文章