OkHttp 3.14.10源码分析(3)- Dispatcher - 线程资源管理和分配

Dispatcher功能是什么?

java doc:

Policy on when async requests are executed.Each dispatcher uses an ExecutorService to run calls internally. If you supply your own executor, it should be able to run the configured maximum number of calls concurrently.

简单翻译就是:

  • 控制异步请求何时执行。
  • 每个dispatcher拥有一个ExecutorService执行异步请求。
  • 用户可配置自定义ExecutorService,要求最大可执行线程至少是maxRequests。
  • maxRequests可通过dispatcher的getMaxRequests()方法获取。
     

接下来简单介绍一下我对dispatcher功能的理解,这样有利于理解后面的内容:

  • Dispatcher主要管理异步请求任务策略,负责分配异步线程资源,控制异步连接数。
  • Dispatcher只覆盖异步任务调度策略层面的逻辑,往下的执行过程对其来说是透明的。
  • 对于同步任务,Dispatcher只是简单记录当前运行的任务任务实体(RealCall),并且是由RealCall主动注册和注销。
  • dispatcher和RealCall、AsyncCall的耦合性比较高,它们之间会相互调用,所以它们的代码往往要相互结合来看。
     

Dispatcher的主要属性

//最大异步任务数,注意是异步不包括同步的。
private int maxRequests = 64;
//对同一个主机的最大异步任务数,同样是异步不包括同步。
private int maxRequestsPerHost = 5;
//请求任务结束,如果当前预执行任务队列为空,线程进入空闲状态会回调该接口。
private @Nullable Runnable idleCallback;
//执行异步任务的线程池。
/** Executes calls. Created lazily. */
private @Nullable 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<>();

maxRequests 和maxRequestsPerHost 为什么只记录异步请求数呢:

  • 如果用户使用单线程 + 同步任务请求,那么同时活跃的任务数肯定只有单个,没必要控制。
  • 如果用户使用多线程或者线程池 + 同步请求的话,那相当于用户自己定制和实现了异步请求策略,那么对于异步请求的管理肯定交给用户是最合适的,OkHttp也很难去管理用户的自定义实现。
  • 用户可以通过配置OkHttpClient来修改dispatcher的属性,从而扩展异步请求的策略。
     

Dispatcher的ExecutorService默认实现

在了解异步任务的执行流程之前,我们先来简单了解一下Dispatcher用来执行异步任务的默认线程池。
代码1:

public synchronized ExecutorService executorService() {
    if (executorService == null) {
    	//0:表示没有核心线程,也就是没有常驻线程。
    	//Integer.MAX_VALUE:表示活跃线程等同于最大整数,活跃线程不会常驻,有最大空闲存活时间限制。
    	//60和TimeUnit.SECONDS:活跃线程的最大空闲存活时间是60秒
    	//new SynchronousQueue<>():同步阻塞队列(大家可以网上找一下这方面资料了解一下),这个队列不存在容器属性,如果消费不及时,生成端put动作会被阻塞。<br/>在这里的效果就是,如果调用了ExecutorService.execute()后,如果没有空闲线程或者还没来得及创建线程,那么execute()会被阻塞,直到有线程来消费。
    	//Util.threadFactory("OkHttp Dispatcher", false):线程工程,创建的线程添加名称前缀OkHttp Dispatcher;创建的线程为守护线程。
    	//第六个参数
		executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
		   new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
}

总结重要的三点:

  • 线程池几乎不限制线程数。
  • 线程默认空闲存活60秒。
  • ExecutorService.execute()方法调用时,如果没有线程及时消费会一直阻塞。

Dispatcher异步任务调度策略

异步任务的执行策略的大概流程:

从上面可以看出涉及Dispatcher的两个关键方法:enqueue(AsyncCall)和promoteAndExecute()。下面就分别来分析这两个方法。

方法:enqueue(AsyncCall)
enqueue方法还没有对AsyncCall进行真正的资源分配和调度,只是对AsyncCall进行一些设置,真正的调度逻辑是由后面的promoteAndExecute()方法实现。
我们先来简单看一下enqueue方法的流程:
 

接着我们分析一下代码:

void enqueue(AsyncCall call) {
    synchronized (this) {
    	//第一步:添加AsyncCall到预执行队列
	    readyAsyncCalls.add(call);
	    //第二步
	    if (!call.get().forWebSocket) {
	      AsyncCall existingCall = findExistingCallWithHost(call.host());
	      if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
	    }
    }
    //第三步
    promoteAndExecute();
}

这方法就三部分,我相信第1、3步大家都是一眼就看穿了,所以就只分析一下第二步,其代码逻辑是设置同一Host的连接计数器:

2.1 同一Host的连接计数器主要是和maxRequestsPerHost属性做比较,目的是控制对同一Host服务器的连接数。

2.2 通过让具有相同Host的AsyncCall对象都共用一个计数器来实现。通过synchronized锁保证同一时间进入代码块的只有一个AsyncCall对象。

  • 通过synchronized锁保证同一时间进入代码块的只有一个AsyncCall对象。
  • 调用findExistingCallWithHost(call.host())方法:查找是否已经存在至少一个相同Host的AsyncCall对象,并且返回任意一个。
@Nullable 
private AsyncCall findExistingCallWithHost(String host) {
    for (AsyncCall existingCall : runningAsyncCalls) {
      if (existingCall.host().equals(host)) return existingCall;
    }
    for (AsyncCall existingCall : readyAsyncCalls) {
      if (existingCall.host().equals(host)) return existingCall;
    }
    return null;
}
  • 如果存在,就把之前AsyncCall对象的计数器也设置给当前的AsyncCall对象;如果不存在就直接使用当前AsyncCall对象的计数器。因为加了锁保护,这样就保证了,如果存在一段连续的时间段,该时间段内一直存在对某Host的异步请求在执行或者等待执行,那么对于该host,后面的AsyncCall对象都是共用第一个AsyncCall对象创建的计数器,直到在某个时间点不再存在连续的异步请求。
    final class AsyncCall extends NamedRunnable {
        private final Callback responseCallback;
        //同一Host的连接计数器
        private volatile AtomicInteger callsPerHost = new AtomicInteger(0);
        ...
        //设置计数器
        void reuseCallsPerHostFrom(AsyncCall other) {
          this.callsPerHost = other.callsPerHost;
        }
        ...

方法:promoteAndExecute()

promoteAndExecute()负责真正对AsyncCall进行资源的调度。
和上面一样,我们还是先来看一下简单的流程:

接着我们在解析一下代码:

private boolean promoteAndExecute() {
    assert (!Thread.holdsLock(this));
    //创建空的可执行AsyncCall集合
    List<AsyncCall> executableCalls = new ArrayList<>();
    boolean isRunning;
    //锁保护
    synchronized (this) {
        //对预执行队列进行迭代循环
      for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
        AsyncCall asyncCall = i.next();
        //正在执行的队列size是否已经>=maxRequests,如果是跳出迭代循环。
        if (runningAsyncCalls.size() >= maxRequests,) break; 
        //判断同一Host的连接计数器的值是否>=maxRequestsPerHost,如果是跳出迭代循环。
        if (asyncCall.callsPerHost().get() >= maxRequestsPerHost,) continue; 
        //从迭代器弹出,也就是从readyAsyncCalls删除了。
        i.remove();
        //同一Host的连接计数器自增1
        asyncCall.callsPerHost().incrementAndGet();
        //添加到可执行集合。
        executableCalls.add(asyncCall);
        //添加到正在执行队列,也就是这时候asyncCall对象已经是被当作执行中状态的了。
        runningAsyncCalls.add(asyncCall);
      }
      isRunning = runningCallsCount() > 0;
    }
    //遍历可执行集合
    for (int i = 0, size = executableCalls.size(); i < size; i++) {
      AsyncCall asyncCall = executableCalls.get(i);
      //调用asyncCall.executeOn方法。
      asyncCall.executeOn(executorService());
    }
    
    return isRunning;
}

代码的重要步骤的解析我都加在上面注释里面了,相信也不难看懂。

但是最后还是要简单介绍一下“asyncCall.executeOn(executorService())”调用的执行逻辑。其实异步任务在线程资源层面的策略,是有OkHttpClient、Dispatcher和Call之间相互协作完成的,所以你单单只看Dispatcher的代码,你可能有点难以勾勒出一个相对清晰和完整的功能流程。

asyncCall.executeOn(executorService())执行流程

在开始理解AsyncCall#executeOn(ExecutorService)执行流程之前,先简单了解AsyncCall的一些基本性质:

  • AsyncCall是NamedRunnable的子类,NamedRunnable实现了Runnable接口,因此AsyncCall对象可以直接作为参数让方法“ExecutorService#execute(Runnable)”执行。
  • NamedRunnable实现了run()方法,run()方法的具体任务逻辑委派给子类execute()方法,因此“executorService#execute(Runnable)”主要执行的是AsyncCall的execute()方法。
     

下面我们来看两个具体的代码片段:

void executeOn(ExecutorService executorService) {
  assert (!Thread.holdsLock(client.dispatcher()));
  boolean success = false;
  try {
    //Call任务被线程池执行
    executorService.execute(this);
    success = true;
  } catch (RejectedExecutionException e) {
    ...
  } finally {
    if (!success) {
        //重点是这里
      client.dispatcher().finished(this);
    }
  }
}

@Override 
protected void execute() {
  boolean signalledCallback = false;
  transmitter.timeoutEnter();
  try {
    Response response = getResponseWithInterceptorChain();
    ...
  } catch (IOException e) {
    ...
  } catch (Throwable t) {
    ...
  } finally {
    //其他都不看,先看这
    client.dispatcher().finished(this);
  }
}

因此接着上面Call.executeOn的流程继续画:

 

Dispatcher#finished(AsyncCall)功能:

  • 同一host连接计数器递减1。
  • 把当前asyncCall对象移出正在执行队列(runningAsyncCalls)。其实到这一步当前的AsyncCall对象的使命就已经完全结束了,后面是Dispatcher自身循环调用的逻辑。
  • 再次调用promoteAndExecute(),从预执行任务队列中拉取任务执行。
  • 如果预执行任务队列已经为空,调用线程空闲回调。

Dispatcher异步任务调度策略小结

到这里异步任务请求AsyncCall在Dispatcher中的整个生命周期就已经理清楚了。Dispatcher只覆盖AsyncCall在线程资源层面的执行策略,再往下的执行过程对其来说是透明的。
AsyncCall到底是从哪里进入Dispatcher的世界的,又在里面发生了什么,最后又是怎么样离它而去的?

Dispatcher同步任务策略

相对于异步任务,Dispatcher对于同步任务的管理是非常简单的,就只有两步:

第一步,同步请求任务RealCall对象在发起请求之前,由RealCall对象主动调用Dispatcher#executed(RealCall)方法,把当前RealCall对象添加到同步任务执行中队列。

注意:同步任务执行中队列是runningSyncCalls,不是runningAsyncCalls,后者是异步任务执行中队列。

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

第二步,同步请求任务结束后,再由RealCall对象主动调用Dispatcher#finished(RealCall)方法,把当前RealCall对象从是runningSyncCalls中移除。

void finished(RealCall call) {
    finished(runningSyncCalls, call);
}

上面的流程都是由RealCall发起的,Dispatcher不存在发起执行的入口,这个和异步是不一样的。

@Override 
public Response execute() throws IOException {
    ...
    try {
        //调用Dispatcher#executed(RealCall)
        client.dispatcher().executed(this);
        //真正执行请求动作
        return getResponseWithInterceptorChain();
    } finally {
        //调用Dispatcher#finished(RealCall)
        client.dispatcher().finished(this);
    }
}

总结

Dispatcher主要管理异步请求任务策略,负责分配异步线程资源,控制异步连接数,只覆盖策略层面的逻辑,往下的执行过程对其来说是透明的。而对于同步任务,Dispatcher只是简单记录当前运行的任务任务实体(RealCall),并且是由RealCall主动注册和注销。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值