上一篇文章讲了Task Graph的处理,在Task的次第承认之后,实在被实行前,还触及到Task的并行调度问题,我们知道gradle是有并行机制的,没有依托关系的Task可以并行实行,以减少构建耗时
除了线程的并行外,gradle甚至供应了进程等级的并行
下面我们来根究一下gradle是怎样确保并行的安全的
Task的并行可以分为2个方面来看
- gradle操控的整体的Task的并行
- Task自身逻辑拆分为多个并行实行
整体Task的并行
先举一个类比,看完这个就能轻松了解gradle Task整体的并行逻辑
有一个工头承包了一个项目,找了一个工程队来帮忙干活
工头先拟定好作业方案表,把任务分为3栏,一个是待准备的,一个是准备好了的,还有一个是正常处理中的
老板给的预算有限,不能让工程队悉数人都上,只能4个人一同干,包括工头自己也要干活
所以准备了4份租约,自己签一份,从工程队招募了3个工人,都进行签约,有必要通过租约来收取任务
还设有一个任务处和一个仓库
任务处担任派发准备好的任务,仓库担任东西的借出、归还
任务处对着方案表看准备好了的那一栏有没有活儿,但不是任务准备好了就能直接初步的,这儿准备好只是任务的前置任务结束了,但是任务能直接初步还有其他前提条件,比方有的任务需求东西,结束任务所需的东西仓库这或许只需一件,也或许有多件,假设这个任务的东西只需一件还被借出了,那也无法初步,所以任务处还得先问下仓库,假设都OK了才华派发这个任务
工头担任整个工程,要check作业方案表上的任务都结束了,任务悉数结束后还要给其他工人解约
再来一张图对整体概念有个了解
工程队就是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都会进入死循环,从任务行列获取任务实行,直到任务行列为空。
下一个任务是通过Plan
的selectNext
办法获取到的,获取下一个任务是需求先获取到锁的,锁的操控是通过ResourceLockCoordinationService
来调度的
CoordinationService
CoordinationService
主要是用来担任和谐worker间资源获取的,中心办法为
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类似签约解约过程
这儿是判别获取lock
的worker
数量是否逾越了最大可答应的parallel数量,没逾越才华签约
WorkSource
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种常见的束缚
- 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时是特别情况,下面讲到的时分会说
- 输出为同一个文件
假设一个task有output
或许local state
(task用来保存自己的缓存的文件目录,像kotlin处理自己的增量编译时有用到)相关注解的特点,那么它归于Producer
,标明task会有输出产生
之前提到过Task是被封装在LocalTaskNode
中的,LocalTaskNode
的实行比较特别,WorkSource
遇到这种Node
,先会给任务方案表中插入一个对应的ResolveMutationNode
,去resolve LocalTaskNode
的mutation
,这个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
仍是先来一张图对整体概念有个理性认知
我们从一个简略的比方来看看怎样运用Worker API
Worker API运用
要运用Worker API,需求先定义WorkParameters
和WorkAction
,下面我们定义了一个CustomParameters
,它只需一个参数index
,CustomAction
只是简略的打印了一下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
我们通过调用submit
将WorkAction
添加到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的实行
总结一下就是
- 假设不运用
await
,那后续的Task不会被block,可以做到parallel
- 假设运用了
await
,那会等悉数work action结束后才实行下一个Task - 不管有没有运用
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