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的好处就体现出来了,他没有容量,那么任务无法被添加到队列中,就会立即创建线程去执行,所以这样的设置可以获得最大的并发量。

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