35. OkHttp之-分發器

注:源碼爲OkHttp 3.10.0版本

在OkHttp內部存在一個Dispatcher的類,他的作用就是通過內部的一個線程池和幾個相關的數據結構來調度請求任務的。我們知道OkHttp的請求任務包含同步請求和異步請求兩種

同步execute()

在類RealCall中,我們可以看到這個方法,這裏有兩行比較重要的代碼,a就是拿到分發器,然後將這個call進行executed,另外一行b就會進入OkHttp第二個核心組成-攔截器-中,這個我們後邊會談到。

    @Override
    public Response execute() throws IOException {
        ....
        try {
            //a
            client.dispatcher().executed(this);
            //b
            Response result = getResponseWithInterceptorChain();
            ....
        } catch (IOException e) {
            ....
        } finally {
            ....
        }
    }
異步enqueue(Callback)
    @Override
    public void enqueue(Callback responseCallback) {
        .....
        client.dispatcher().enqueue(new AsyncCall(responseCallback));
    }

client就是OkHttpClient,可以看到dispatcher默認情況下就是一個new出來的Dispatcher類,或者我們也可以在初始化的時候定製一個自己的dispatcher傳入進來。

其實不只是同步請求,異步請求也用到了dispatcher,看下邊,所以我們可以知道,同步請求和異步請求最終都是進入到了dispatcher分發器中。

Dispatcher的任務調度

在分發器中,存在幾個比較重要的成員

    //異步請求可以同時存在的最大請求數
    private int maxRequests = 64;
    //異步請求同一個域名可以同時存在的最大請求數
    private int maxRequestsPerHost = 5;
    //沒有請求時可以執行的一些任務,可以由調用者傳入進來,一般用不到
    private @Nullable Runnable idleCallback;

    //分發器的線程池,有默認實現,也可自定義傳入
    private @Nullable ExecutorService executorService;

    //異步請求等待執行隊列,超過64以外或者統一域名大於5的時候,任務被添加到這個隊列
    private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

    //異步請求正在執行的隊列,除上邊條件外的任務會被添加到這裏
    private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

    //同步任務會被添加到這個隊列,並且沒有長度限制
    private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

分發器的同步方法很簡單,只是把任務添加到了runningSyncCalls隊列

    synchronized void executed(RealCall call) {
        runningSyncCalls.add(call);
    }

所以我們重點看下異步方法

    synchronized void enqueue(AsyncCall call) {
        //1、如果正在執行的請求小於64
        // 2、相同host的請求不能超過5個
        if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
            //添加到正在執行的任務隊列
            runningAsyncCalls.add(call);  
            //丟到線程池開始執行
            executorService().execute(call);
        } else {
            //不滿足則存入等待執行隊列
            readyAsyncCalls.add(call);
        }
    }

邏輯很簡單,但是等待隊列中的任務是怎麼被執行的呢?既然必須滿足正在執行的任務不超過64個,同一域名的任務不超過5個,那麼每次有任務執行完成的時候判斷一下會是一個非常好的將等待任務開始執行的時機,OkHttp也正是這樣做的,執行完 一個請求後,都會調用分發器的 finished方法

     //Used by {@code AsyncCall#run} to signal completion.
    void finished(AsyncCall call) {
        finished(runningAsyncCalls, call, true);
    }
    void finished(RealCall call) {
        finished(runningSyncCalls, call, false);
    }
    //第三個參數promoteCalls表示是否是異步方法
    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;
        }
        //這裏對應的是我們上邊提到的閒時任務,當正在執行的任務爲0時,開始執行這個任務
        if (runningCallsCount == 0 && idleCallback != null) {
            idleCallback.run();
        }
    }

可以看到,當finish方法執行的時候,會將執行完成的這個任務從正在執行的任務隊列中remove掉,並且,如果本次remove的是異步任務,會繼續執行promoteCalls方法,我們看看這裏做了什麼。可以看到,它先判斷了當前是否滿足將等待任務開始執行的條件,如果滿足,會遍歷整個等待執行的任務隊列,把滿足條件的等待任務添加到正在執行的任務隊列並立即執行這個任務

    private void promoteCalls() {
        //正在執行的任務數如果大於64,返回
        if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
        //如果等待執行的任務數爲0,那麼也返回,因爲這個方法的目的本身就是
        //爲了把等待的任務變成正在執行的任務
        if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
        //滿足上邊兩個條件,等待任務就可以嘗試執行了
        for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
            AsyncCall call = i.next();
            // 同一Host請求只能同時有5個
            if (runningCallsForHost(call) < maxRequestsPerHost) {
                i.remove();
                runningAsyncCalls.add(call);
                executorService().execute(call);
            }

            if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
        }
    }
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.首先他的和姓線程數爲0,表示線程池不會一直緩存線程,當一個線程閒置了60s之後就會被回收
2.然後,他的最大線程數爲Integer.MAX_VALUE,可以保證來多少任務創建多少個線程,提供最大的併發量,當然由於前邊提到的64的限制,所以最多也只會有64個線程同時運行
3.最後線程池的等待隊列爲SynchronousQueue,我們知道SynchronousQueue內部沒有存放元素的能力,他不像ArrayBlockingQueue或者LinkedBlockingQueue可以指定隊列的容量,SynchronousQueue容量爲0。那麼爲什麼OkHttp要使用這個隊列呢?其實我們可以想一下線程池的執行原理

首先創建核心線程執行任務,核心線程數如果已經滿了,沒有閒置的核心線程那麼就將任務加入等待隊列執行,最後如果等待隊列也滿了,而且總線程數還沒達到最大線程數,這個時候纔會創建非核心線程執行任務。

那麼可以想象一個,如果使用LinkedBlockingQueue這種有容量的隊列會怎樣?假設隊列長度爲10,那麼因爲核心線程數爲0,那麼前十個任務都會被加入等待隊列,並且一直沒有被執行的機會。而使用SynchronousQueue的好處就體現出來了,他沒有容量,那麼任務無法被添加到隊列中,就會立即創建線程去執行,所以這樣的設置可以獲得最大的併發量。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章