上一篇文章讲了Task Graph的处理,在Task的次第承认之后,实在被实行前,还触及到Task的并行调度问题,我们知道gradle是有并行机制的,没有依托关系的Task可以并行实行,以减少构建耗时
除了线程的并行外,gradle甚至供应了进程等级的并行
下面我们来根究一下gradle是怎样确保并行的安全

Task的并行可以分为2个方面来看

  1. gradle操控的整体的Task的并行
  2. Task自身逻辑拆分为多个并行实行

整体Task的并行

先举一个类比,看完这个就能轻松了解gradle Task整体的并行逻辑

有一个工头承包了一个项目,找了一个工程队来帮忙干活
工头先拟定好作业方案表,把任务分为3栏,一个是待准备的,一个是准备好了的,还有一个是正常处理中的
老板给的预算有限,不能让工程队悉数人都上,只能4个人一同干,包括工头自己也要干活
所以准备了4份租约,自己签一份,从工程队招募了3个工人,都进行签约,有必要通过租约来收取任务
还设有一个任务处和一个仓库
任务处担任派发准备好的任务,仓库担任东西的借出、归还
任务处对着方案表看准备好了的那一栏有没有活儿,但不是任务准备好了就能直接初步的,这儿准备好只是任务的前置任务结束了,但是任务能直接初步还有其他前提条件,比方有的任务需求东西,结束任务所需的东西仓库这或许只需一件,也或许有多件,假设这个任务的东西只需一件还被借出了,那也无法初步,所以任务处还得先问下仓库,假设都OK了才华派发这个任务
工头担任整个工程,要check作业方案表上的任务都结束了,任务悉数结束后还要给其他工人解约

再来一张图对整体概念有个了解

Gradle深化解析 - Task原理(并行篇)

工程队就是ExecutorService
工人就是Worker,其中心就是ExecutorService供应的Thread
最多多少人能一同干活看CPU当时最大可用中心数,默许最大一同工作worker数量是通过Runtime.getRuntime().availableProcessors()获取的,当然也可以通过参数配备手动指定
仓库就是WorkSource
任务处就是CoordinationService

gradle在拟定好实行方案后,Task的实行是由PlanExecutor来处理的

gradle运用的是Executors.newCachedThreadPool()创立的worker线程池,其创立的线程名称会以Execution worker作为前缀

按 (最大可运用中心数量 – 1) 创立worker,由于自己自身线程也会充任worker工作任务,并且需求承担其他worker结束任务后的主导作业,可以认为是主线程

创立完后,每个worker都会进入死循环,从任务行列获取任务实行,直到任务行列为空。
下一个任务是通过PlanselectNext办法获取到的,获取下一个任务是需求先获取到锁的,锁的操控是通过ResourceLockCoordinationService来调度的

CoordinationService

CoordinationService主要是用来担任和谐worker间资源获取的,中心办法为

Gradle深化解析 - Task原理(并行篇)

stateLockAction标明在这几种情况间改变,类似情况机的操控逻辑。stateLockAction是synchronized同步的,通过state lock操控一同只需一个worker可以实行,worker在这儿去获取所需的悉数资源锁,通过其回来值ResourceLockState.Disposition将获取锁的效果告知给CoordinationService
ResourceLockState.Disposition的值代表获取资源锁的效果,有3种类型

  • FINISHED
    悉数需求的锁都获取到了,可以开释state lock
  • FAILED
    有需求的锁没有获取到,需求回滚到之前的情况,开释悉数已获取到的锁,并开释state lock
  • RETRY
    有需求的锁没有获取到,需求回滚到之前的情况,开释悉数已获取到的锁,并堵塞等候state改变,就是调用lock.wait,自身进入block情况,开释state lock,等候其他线程实行完然后notify自己从头实行。这种情况的产生,或许是由于worker数量到达上限了或许任务的前置条件没有结束等

stateLockAction中首要通过WorkerLeaseService来获取锁,类似上面类比比方中提到的签定租约,只需签定成功了才华找WorkSource收取任务,假设租约数量到达上限了就无法签,那会回来RETRY,进入block情况等候有人解约

WorkerLease
WorkerLeaseService是供应WorkerLease的服务,锁的获取是由它担任的,lock和unlock类似签约解约过程
这儿是判别获取lockworker数量是否逾越了最大可答应的parallel数量,没逾越才华签约

WorkSource

Gradle深化解析 - Task原理(并行篇)

WorkSource类似于一个仓库,它将任务划分在3个集结中

  • waitingToStartNodes 等候中行列,坐落这儿的任务,其依托的前置task有或许还未结束,假设结束了会将它移入到readyNodes
  • readyNodes readyNodes标明有任务ready了,其依托的task都结束了,但并非可以当即初步,还受限于其他的锁的情况,下面会具体阐述
  • runningNodes 处于工作中的任务

依据这3个集结情况,WorkSource会有3种情况

  • MaybeWorkReadyToStart 或许有work现已ready可以初步作业
    waitingToStartNodes或许readyNodes不为空时,代表着这个情况
  • NoMoreWorkToStart 假设waitingToStartNodes为空,标明没有任务需求实行了,任务悉数派发出去了。假设runningNodes此时也是空的,那标明任务悉数结束了
  • NoWorkReadyToStart waitingToStartNodes不为空,但是readyNodes为空时,标明还有任务等候实行,但没有任务ready,构成这种情况的原因或许是waitingToStartNodes的task依托的task还没有结束,只需等依托的task结束后才会被加入到readyNodes

ReadyNodes

WorkSource就是不断的从readyNodes中拿出任务,交给worker去处理的 上面提到过了readyNodes中拿出的任务纷歧定可以立刻初步实行,下面罗列2种常见的束缚

  1. project间的并行

实践上gradle实行task默许是按序一个一个实行的,不会并行实行
但是假设设置了答应parallel,并且是多project项目时,就可以有一同工作task的或许了
每个project都会对应有一把锁,锁保存在一同,通过project的途径区别
打开了parallel的话,project对应的锁用的是自身project的途径的,没有打开的情况,用的是root project的途径
也就是说打开了parallel的情况下,project间是可以并行工作的,但是每个project内的task仍是按序一个一个实行
没有打开parallel的情况,悉数的task都一个一个实行,不管它是来自哪个project的
可以认为打开parallel的时分,每个project都有独立的管道,没有打开的时分共用一条管道

关于parallel,运用worker api时是特别情况,下面讲到的时分会说

  1. 输出为同一个文件

假设一个task有output或许local state(task用来保存自己的缓存的文件目录,像kotlin处理自己的增量编译时有用到)相关注解的特点,那么它归于Producer,标明task会有输出产生
之前提到过Task是被封装在LocalTaskNode中的,LocalTaskNode的实行比较特别,WorkSource遇到这种Node,先会给任务方案表中插入一个对应的ResolveMutationNode,去resolve LocalTaskNodemutation,这个mutation又是什么呢

mutation的意思是变化,这儿指task是否会对外产生影响,主要指是否有生成文件,删除文件等,mutation包括有outputs文件途径,localstate,是否有input files等信息。在Task Graph篇中提到过inputs/outputs分析,这儿是同样的办法,用Visitor去访问inputs/outputs的特点将mutation信息提取出来

mutation提取出的输出文件途径信息会和LocalTaskNode相关起来保存在ExecutionNodeAccessHierarchy

有相同输出途径的Producer不答应一同实行,这一点可以通过途径从ExecutionNodeAccessHierarchy查找是否有对应LocalTaskNode正在实行判别出来
假设正在实行的task有outputs是同样文件,或许其文件目录包括了想要工作的task的输出文件的话,gradle是不会当即实行的,需求工作中的task结束任务开释锁之后才华实行

这儿只是描绘了一下整体的任务实行派发的流程,还有许多细节其实没有触及的,例如任务被撤销的处理,运用了--continue疏忽失败的任务继续进行的情况等等

Task内并行

假设想要在Task内异步实行逻辑,其实会有许多问题
比方在Task内手动发起线程,线程内的逻辑异步实行的时分,gradle会认为Task现已实行完了,后续依托于此的Task就有或许直接实行,或许异步逻辑还未实行完,整体Task的实行流程现已结束,异步逻辑的实行效果无法得到确保

官方供应了异步机制Worker API来处理这些问题

Worker API

仍是先来一张图对整体概念有个理性认知

Gradle深化解析 - Task原理(并行篇)

我们从一个简略的比方来看看怎样运用Worker API

Worker API运用

要运用Worker API,需求先定义WorkParametersWorkAction,下面我们定义了一个CustomParameters,它只需一个参数indexCustomAction只是简略的打印了一下index和地点线程信息

interface CustomParameters extends WorkParameters {
    Property<Integer> getIndex()  
}  
abstract class CustomAction implements WorkAction<CustomParameters> {  
    @Override  
    void execute() {  
        println "CustomAction: ${parameters.index.get()} in thread: ${Thread.currentThread()}"
    }
}

然后是在Task中运用WorkAction

abstract class CustomTask extends DefaultTask {
    @Inject  
    abstract WorkerExecutor getWorkerExecutor()  
    @TaskAction  
    void action() {  
        WorkQueue workQueue = workerExecutor.noIsolation()  
        6.times {  
            workQueue.submit(CustomAction.class, { parameters ->  
                parameters.index = it  
            })  
        }  
    }  
}

首要我们需求一个WorkerExecutor,它是由gradle通过依托注入给我们的,我们也无法自己初始化它,所以需求给它注解上@javax.inject.Inject

调用workerExecutor.noIsolation()获取到WorkQueue,在获取到WorkQueue后,调用它的submit办法,传入WorkAction的class对象和WorkParameters的初始化action就结束了
(noIsolation先按下不表,后面会进行说明)

这儿我们简略的提交了6个WorkAction,来看看实行效果

CustomAction: 2 in thread: Thread[WorkerExecutor Queue Thread 3,5,main]
CustomAction: 0 in thread: Thread[WorkerExecutor Queue,5,main]
CustomAction: 1 in thread: Thread[WorkerExecutor Queue Thread 2,5,main]
CustomAction: 3 in thread: Thread[WorkerExecutor Queue Thread 4,5,main]
CustomAction: 5 in thread: Thread[WorkerExecutor Queue,5,main]
CustomAction: 4 in thread: Thread[WorkerExecutor Queue Thread 3,5,main]

由于是异步实行,WorkAction实行的先后次第并不承认,上面是一种或许的输出情况

从上面的比方可以看出Worker API有3个重要的类

WorkAction
WorkQueue
WorkerExecutor

我们通过调用submitWorkAction添加到WorkQueue中,然后WorkerExecutor从中取出WorkAction进行实行,具体安排在哪个线程实行也是WorkerExecutor来担任处理

这儿的线程数量也会遭到parallel配备org.gradle.workers.max的影响,和整体Task间并行运用的线程数量加起来不能逾越这个束缚

异步任务实行确保

submit之后WorkAction就初步被安排在其他线程实行了,Task action的也就到此结束了,但是假设不想action就此退出的话,可以submit之后调用workQueue.await,它会让当时线程等候悉数WorkAction结束任务

await的运用不是有必要的,即便没有自动调用,gradle也能自动等候悉数WorkAction的结束
这是通过AsyncWorkTracker来实现的
望文生义,AsyncWorkTracker是用来寻找悉数异步任务的,在action实行完后,AsyncWorkTracker会wait等候悉数WorkAction的结束,在其wait期间会开释project锁等资源,这样就让后续的Task也有并行实行的机会了

但是自动调用await产生在Task action的内部,在AsyncWorkTracker之前,而await是不会开释锁的,所以会block后续Task的实行

总结一下就是

  1. 假设不运用await,那后续的Task不会被block,可以做到parallel
  2. 假设运用了await,那会等悉数work action结束后才实行下一个Task
  3. 不管有没有运用await,gradle都会确保WorkAction异步任务的实行,gradle不会先于异步任务结束而结束,并且依托它的Task不会在其异步任务结束前就初步实行

运用Worker API可以享遭到2个好处

  • 并行实行Task自身的逻辑
  • 可以让后续任务parallel起来

Isolation

上面我们的比方中获取WorkQueue时运用的是workerExecutor.noIsolation(),这个noIsolation其实是一种隔绝方式

Worker API有3种隔绝方式

noIsolation classLoaderIsolation processIsolation

noIsolation标明没有任何隔绝办法

classLoaderIsolation是classloader等级隔绝
这种情况一般产生于编译时用到的java版别和实行gradle的纷歧同
比方编译用的是java 8,而实行gradle用的是java 11,假设不进行classloader隔绝,就会用java 11去编译代码,会有导致代码兼容性问题产生的或许

processIsolation进程间隔绝,是等级最高的隔绝办法,它会发起后台Daemon进程来实行WorkAction

Daemon Process

我们来看看processIsolation是怎样调度Daemon进程的

运用这种隔绝办法,在上面的线程结构基础上,WorkExecutor的子线程和进程进行通讯来结束WorkAction

首要它们会去缓存中检查是否有搁置的进程,有的话就复用,没有的话就从头发起一个进程,复用需求forkOptions共同,只能获取到以相同forkOptions发起的进程,forkOptions是在获取WorkQueue的时分设置的,它可以用来配备heapSize、环境变量等

发起进程是运用的ProcessBuilder.start的办法,一同实行命令 java GradleWorkerMain(简化后的,实践还有许多classpath,参数等等),就是通过java来实行GradleWorkerMain

GradleWorkerMain就是Daemon进程的实行入口了,它和主进程通过socket通讯

主进程担任将WorkAction的参数,WorkAction的类型信息等数据安排好,进行序列化传输给Daemon进程

Daemon进程从InputStream读取主进程发过去的指令,将数据反序列化出来,反射实例化Work Action进行实行,然后将实行效果回来给主进程

虽然是在独立的进程实行,在异步任务实行确保部分说的规矩同样是适用的

参考资料

Developing Parallel Tasks using the Worker API
Developing Custom Gradle Task Types