目录
前言
协程并不是kotlin发明的,但是在kotlin中协程是真的好用。但是协程的初衷是单线程中任务的调度,可以充分利用线程资源,如果我们任务多到单线程已经无法及时处理怎么办?
脑海中的第一反应就是多线程处理,通过线程池进行管理。但是java传统的线程池是无法满足kotlin协程的灵活调度的。为什么?不妨来想一下,当用线程1来执行任务,这时候来了一个新的任务,是用线程1执行还是新开线程执行?最佳的方案是当线程1繁忙的时候新开线程来执行,否则用线程1执行,但是java传统的线程池是不具备这种功能的。
那么我们需要改造java线程池来满足需求么?
当然不需要,kotlin已经为我们实现了可以灵活调度协程的线程池——CoroutineDispatcher
CoroutineDispatcher
CoroutineDispatcher是一个抽象类,不过kotlin中已经提供了几个实现,都在Dispatchers中:
- Default: 用于处理cup密集型的任务
- Main:处理主线程(即UI线程)的任务
- IO:用于处理IO密集型任务
可以看到一般情况下我们不必自己去实现CoroutineDispatcher,现有的已经完全可以满足我们的需求。
使用起来也很简单,在使用launch的时候添加即可,如下:
launch(Dispatchers.Default) {
...
}
launch这个函数实际上有三个参数
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
其中前两个参数是有默认值的,其中第一个参数context是CoroutineContext类型,而CoroutineDispatcher最终也继承了CoroutineContext。当为context赋值后,就会使用对应的线程池来分配处理任务。
那么这些线程池是如何实现对协程的灵活分配的?
Dispatchers.Default
先从Dispatchers.Default
来看,在Dispatchers是这样定义的
@JvmStatic
public actual val Default: CoroutineDispatcher = DefaultScheduler
实际上是DefaultScheduler,而它则继承了SchedulerCoroutineDispatcher
internal object DefaultScheduler : SchedulerCoroutineDispatcher(
CORE_POOL_SIZE, MAX_POOL_SIZE,
IDLE_WORKER_KEEP_ALIVE_NS, DEFAULT_SCHEDULER_NAME
) {
...
}
这里面有几个参数,大家看着应该很眼熟,跟java线程池的那几个参数很像:核心线程数、最大线程数、存活时间,那么这几个数都是多少呢
线程池参数
CORE_POOL_SIZE
核心线程数定义如下:
@JvmField
internal val CORE_POOL_SIZE = systemProp(
"kotlinx.coroutines.scheduler.core.pool.size",
AVAILABLE_PROCESSORS.coerceAtLeast(2),
minValue = CoroutineScheduler.MIN_SUPPORTED_POOL_SIZE
)
这里看并不是一个固定的值,而是通过systemProp函数来得到的,这个函数代码如下:
internal fun systemProp(
propertyName: String,
defaultValue: Long,
minValue: Long = 1,
maxValue: Long = Long.MAX_VALUE
): Long {
val value = systemProp(propertyName) ?: return defaultValue
val parsed = value.toLongOrNull()
?: error("System property '$propertyName' has unrecognized value '$value'")
if (parsed !in minValue..maxValue) {
error("System property '$propertyName' should be in range $minValue..$maxValue, but is '$parsed'")
}
return parsed
}
一共四个参数,命名一目了然就不一一说了,这个函数的简单来说就是获取对应名称的系统属性,如果没有则使用默认值,然后将这个值与最大最小值比较校验。
返回去看CORE_POOL_SIZE,最大值没设置所以就算MAX_VALUE,最小值MIN_SUPPORTED_POOL_SIZE是1,最关键的还是默认值是
AVAILABLE_PROCESSORS.coerceAtLeast(2)
AVAILABLE_PROCESSORS定义是
internal val AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors()
就是当前可用处理器的数量,coerceAtLeast(2)
表示下限是2,所以默认值就是当前可用处理器数量,如果小于2则是2。
这样CORE_POOL_SIZE也就很清晰了,可以很清楚的看到线程数是与CPU核心数相关的,这样可以充分利用CPU资源也不会创建过多的线程。
MAX_POOL_SIZE
最大线程数定义如下
@JvmField
internal val MAX_POOL_SIZE = systemProp(
"kotlinx.coroutines.scheduler.max.pool.size",
CoroutineScheduler.MAX_SUPPORTED_POOL_SIZE,
maxValue = CoroutineScheduler.MAX_SUPPORTED_POOL_SIZE
)
这个就更简单了,默认值和最大值是一样的,都是MAX_SUPPORTED_POOL_SIZE,这个数是(1 shl 21) - 2
IDLE_WORKER_KEEP_ALIVE_NS
存活时间定义如下
@JvmField
internal val IDLE_WORKER_KEEP_ALIVE_NS = TimeUnit.SECONDS.toNanos(
systemProp("kotlinx.coroutines.scheduler.keep.alive.sec", 60L)
)
可以看到没有系统设置的话,最大就是60秒
CoroutineScheduler
上面这几个参数最终是在CoroutineScheduler中发挥作用的,回到一开始的SchedulerCoroutineDispatcher中,会创建一个CoroutineScheduler,如下:
private var coroutineScheduler = createScheduler()
private fun createScheduler() =
CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)
这个CoroutineScheduler就是用来管理线程池,进行任务分发的调度器。
其实无论是Dispatchers.Default还是Dispatchers.IO,最终都是通过CoroutineScheduler来管理线程池,只不过在线程池的参数和一些其他细节有些许不一样。下面来看看分发的过程是什么样的。
任务分发
任务分发是核心功能,是由dispatch函数来实现的,最终是在CoroutineScheduler的dispatch函数来处理,代码如下:
fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, tailDispatch: Boolean = false) {
trackTask() // this is needed for virtual time support
val task = createTask(block, taskContext) //1
// try to submit the task to the local queue and act depending on the result
val currentWorker = currentWorker() //2
val notAdded = currentWorker.submitToLocalQueue(task, tailDispatch) //3
if (notAdded != null) {
if (!addToGlobalQueue(notAdded)) { //4
// Global queue is closed in the last step of close/shutdown -- no more tasks should be accepted
throw RejectedExecutionException("$schedulerName was terminated")
}
}
val skipUnpark = tailDispatch && currentWorker != null //5
// Checking 'task' instead of 'notAdded' is completely okay
if (task.mode == TASK_NON_BLOCKING) {
if (skipUnpark) return
signalCpuWork() //6
} else {
// Increment blocking tasks anyway
signalBlockingWork(skipUnpark = skipUnpark) //7
}
}
可以看到代码1和代码2分别创建Task和获取Worker,这也是整个线程池中的两个重要概念
task
task就是任务,实际上就是将我们要执行的代码封装成Runnable,源码如下:
internal abstract class Task(
@JvmField var submissionTime: Long,
@JvmField var taskContext: TaskContext
) : Runnable {
constructor() : this(0, NonBlockingContext)
inline val mode: Int get() = taskContext.taskMode // TASK_XXX
}
可以看到它继承Runnable,有一个很重要的属性——mode,标记这个任务是不是阻塞的,有两种状态:
- TASK_NON_BLOCKING:非阻塞任务,计算密集型任务
- TASK_PROBABLY_BLOCKING:可能会阻塞
这个mode是taskContext带过来的,taskContext就是dispatch中的第二个参数,可以看到在Dispatchers.Default中这个参数是NonBlockingContext,所以通过它执行的task都是TASK_NON_BLOCKING状态。同理可以想到Dispatchers.IO的任务应该就是TASK_PROBABLY_BLOCKING状态。
后面可以看到针对不同状态的Task会有不同的处理。
worker
worker其实就是线程,它的源码很大,后面我们讲分发还会涉及到,所以这里就先不贴出来了。它实际上是继承了Thread,比较重要的是它内部维护了一个localQueue队列,这个队列也是CoroutineDispatcher的一个比较重要部分,后面会遇到。
回头看获取Worker的函数是currentWorker,代码如下:
private fun currentWorker(): Worker? = (Thread.currentThread() as? Worker)?.takeIf { it.scheduler == this }
可以看到就是获取了当前的线程,因为协程代码是在一个Worker中执行,所以当前线程也是一个Worker对象。
本地队列和全局队列
继续看dispatch的代码,代码3处执行了submitToLocalQueue,故名思义是将这个任务加入到本地队列中,这个本地队列就是上面Worker内部维护的localQueue。
继续看代码4,如果提交失败则将任务加入到全局队列。
本地队列
全局队列先放一边,我们来看看提交失败的情况有哪些。看看submitToLocalQueue的实现:
private fun Worker?.submitToLocalQueue(task: Task, tailDispatch: Boolean): Task? {
if (this == null) return task
if (state === WorkerState.TERMINATED) return task
if (task.mode == TASK_NON_BLOCKING && state === WorkerState.BLOCKING) {
return task
}
mayHaveLocalTasks = true
return localQueue.add(task, fair = tailDispatch)
}
如果当前Worker状态是停止(TERMINATED),提交失败;如果是非阻塞任务(即计算密集型任务)而当前Worker是阻塞状态,也提交失败(这是因为计算密集型任务要尽快执行,如果当前Worker是阻塞的,无法知道何时能执行到,所以就不向这个Worker提交这种任务)。
但是提交失败不仅仅是这些,来看看localQueue的add函数:
fun add(task: Task, fair: Boolean = false): Task? {
if (fair) return addLast(task)
val previous = lastScheduledTask.getAndSet(task) ?: return null
return addLast(previous)
}
private fun addLast(task: Task): Task? {
if (task.isBlocking) blockingTasksInBuffer.incrementAndGet()
if (bufferSize == BUFFER_CAPACITY - 1) return task
val nextIndex = producerIndex.value and MASK
while (buffer[nextIndex] != null) {
Thread.yield()
}
buffer.lazySet(nextIndex, task)
producerIndex.incrementAndGet()
return null
}
在addLast函数中还有一个容量校验,也就是说localQueue还有一个容量限制BUFFER_CAPACITY(128),所以当localQueue中的任务超过这个也会提交失败。
全局队列
当本地队列添加失败,就会添加到全局队列,addToGlobalQueue的代码如下:
private fun addToGlobalQueue(task: Task): Boolean {
return if (task.isBlocking) {
globalBlockingQueue.addLast(task)
} else {
globalCpuQueue.addLast(task)
}
}
可以看到全局队列有两个globalBlockingQueue和globalCpuQueue,分别装载阻塞任务和CPU任务。这两个队列是CoroutineScheduler的属性。
这里可以看到协程线程池的一个调度技巧,当一个线程繁忙(阻塞或者队列任务太多)的时候,会将后续的任务放到全局队列中,后续就会根据当前线程数来判断是否创建新的线程来执行这些任务。那么这块是如何实现的?
启动Worker
继续看dispatch的代码,在将任务加入队列后,下一步就是启动worker来执行任务。
先是在代码5处先判断当前worker是不是空,如果不为空且tailDispatch是true则skipUnpark是true。
skipUnpark如果是true,则跳过后面唤起和创建worker的过程。
继续看下面的代码,可以看到通过当前任务的阻塞状态来执行不同的代码signalCpuWork
和signalBlockingWork
,不过对比这两个函数的代码
private fun signalBlockingWork(skipUnpark: Boolean) {
// Use state snapshot to avoid thread overprovision
val stateSnapshot = incrementBlockingTasks()
if (skipUnpark) return
if (tryUnpark()) return
if (tryCreateWorker(stateSnapshot)) return
tryUnpark() // Try unpark again in case there was race between permit release and parking
}
fun signalCpuWork() {
if (tryUnpark()) return
if (tryCreateWorker()) return
tryUnpark()
}
可以看到两个函数大致的流程是一样的,先通过tryUnpark
函数来唤醒worker执行任务,如果唤醒失败则通过tryCreateWorker
来创建Worker,然后再通过tryUnpark
唤醒。
唤醒Worker
tryUnpark
源码如下:
private fun tryUnpark(): Boolean {
while (true) {
val worker = parkedWorkersStackPop() ?: return false
if (worker.workerCtl.compareAndSet(PARKED, CLAIMED)) {
LockSupport.unpark(worker)
return true
}
}
}
首先通过parkedWorkersStackPop
来获取worker,来看看这部分细节:
private fun parkedWorkersStackPop(): Worker? {
parkedWorkersStack.loop { top ->
val index = (top and PARKED_INDEX_MASK).toInt()
val worker = workers[index] ?: return null //1
val updVersion = (top + PARKED_VERSION_INC) and PARKED_VERSION_MASK
val updIndex = parkedWorkersStackNextIndex(worker)
if (updIndex < 0) return@loop
if (parkedWorkersStack.compareAndSet(top, updVersion or updIndex.toLong())) {
worker.nextParkedWorker = NOT_IN_STACK
return worker
}
}
}
parkedWorkersStack存储的是所有挂起Worker的索引。然后在代码1处,这里的workers是CoroutineScheduler的一个变量,这是一个全局性的列表,存储了当前所有的Worker。这里就是从这个列表中取出最后一个挂起的Worker来唤醒。
创建Worker
如果唤醒失败就会重新创建一个Worker,代码如下
private fun tryCreateWorker(state: Long = controlState.value): Boolean {
val created = createdWorkers(state)
val blocking = blockingTasks(state)
val cpuWorkers = (created - blocking).coerceAtLeast(0)
if (cpuWorkers < corePoolSize) {
val newCpuWorkers = createNewWorker()
if (newCpuWorkers == 1 && corePoolSize > 1) createNewWorker()
if (newCpuWorkers > 0) return true
}
return false
}
这里可以看到有一个cpuWorkers的概念,它是当前所有Worker的数量减去当前阻塞任务的数量(即当前被阻塞任务占用的Worker数量),然后用这个cpuWorkers去跟corePoolSize比较,如果超过就不创建。所以这里也是与java传统线程池不一样的地方,corePoolSize对应的是执行计算密集型Worker的核心数量,这样可以让CPU充分发挥作用。(阻塞任务一般都是IO密集型,主要时间都是等待IO操作,这时候是不占用CPU资源的)
创建createNewWorker
的源码如下
private fun createNewWorker(): Int {
synchronized(workers) {
// Make sure we're not trying to resurrect terminated scheduler
if (isTerminated) return -1
val state = controlState.value
val created = createdWorkers(state)
val blocking = blockingTasks(state)
val cpuWorkers = (created - blocking).coerceAtLeast(0)
// Double check for overprovision
if (cpuWorkers >= corePoolSize) return 0
if (created >= maxPoolSize) return 0
val newIndex = createdWorkers + 1
require(newIndex > 0 && workers[newIndex] == null)
val worker = Worker(newIndex) //1
workers.setSynchronized(newIndex, worker) //2
require(newIndex == incrementCreatedWorkers())
worker.start() //3
return cpuWorkers + 1
}
}
这里同样用cpuWorkers与corePoolSize,但是注意与maxPoolSize对比就是全部Worker的数量,所以核心数量和最大数量对比的角度是不一样的。如果可以创建新Worker,则new一个Worker出来(代码1),然后添加到全局的workers中(代码2),最后启动这个Worker(代码3)。
执行任务
Worker是一个Thread,所以它的任务执行是在run函数中的,在Worker中run函数会直接调用runWorker函数,代码如下:
private fun runWorker() {
var rescanned = false
while (!isTerminated && state != WorkerState.TERMINATED) {
val task = findTask(mayHaveLocalTasks) //1
// Task found. Execute and repeat
if (task != null) {
rescanned = false
minDelayUntilStealableTaskNs = 0L
executeTask(task) //2
continue
} else {
mayHaveLocalTasks = false
}
...
tryPark() //3
}
tryReleaseCpu(WorkerState.TERMINATED) //4
}
来关注一下关键代码,可以看到这里有一个while循环,这样可以不停的处理任务,在代码1处通过findTask
来获取一个任务;如果有任务则在代码2处通过executeTask
函数来执行任务,执行完成重新获取任务;如果没有任务会通过tryPark
(代码3处)来挂起线程,等待新任务的到来后再激活线程继续执行任务。最后如果线程结束,退出while循环会执行tryReleaseCpu
(代码4)来释放Cpu资源。
所以这里最主要的部分是findTask
函数,它获取任务的逻辑是什么?
获取任务
获取任务的源码如下:
fun findTask(scanLocalQueue: Boolean): Task? {
if (tryAcquireCpuPermit()) return findAnyTask(scanLocalQueue)
// If we can't acquire a CPU permit -- attempt to find blocking task
val task = if (scanLocalQueue) {
localQueue.poll() ?: globalBlockingQueue.removeFirstOrNull()
} else {
globalBlockingQueue.removeFirstOrNull()
}
return task ?: trySteal(blockingOnly = true)
}
函数一开始如果tryAcquireCpuPermit
是true,说明获得了CPU token,那么它就可以执行非阻塞任务(计算密集型任务),也可以执行阻塞任务,所以调用findAnyTask
函数来获取任务;如果是false,则只能执行阻塞任务(因为无法立刻获取CPU资源来执行任务,而计算密集型任务应该尽快执行,所以这个Worker当前就不应该来执行非阻塞任务)。
那么对比看一下findAnyTask
源码:
private fun findAnyTask(scanLocalQueue: Boolean): Task? {
if (scanLocalQueue) {
val globalFirst = nextInt(2 * corePoolSize) == 0
if (globalFirst) pollGlobalQueues()?.let { return it }
localQueue.poll()?.let { return it }
if (!globalFirst) pollGlobalQueues()?.let { return it }
} else {
pollGlobalQueues()?.let { return it }
}
return trySteal(blockingOnly = false)
}
可以看到两个函数获取任务的逻辑大致相同,一般情况下都是先在localQueue中获取任务,如果没有任务则会在globalBlockingQueue中获取任务,这样就保证了任务调度的灵活性。
findAnyTask
中获取全局任务用的是pollGlobalQueues
这个函数,这里是通过一个随机数来在前面提到的两个全局队列中获取任务的,如下:
private fun pollGlobalQueues(): Task? {
if (nextInt(2) == 0) {
globalCpuQueue.removeFirstOrNull()?.let { return it }
return globalBlockingQueue.removeFirstOrNull()
} else {
globalBlockingQueue.removeFirstOrNull()?.let { return it }
return globalCpuQueue.removeFirstOrNull()
}
}
所以,一个Worker并不是完全固定执行某一种任务,这样就更加灵活。
注意在两个函数的最后都有一个trySteal
函数,这个就很灵性,它是用来偷取任务的,那么它是如何工作的?
偷取任务
在本地任务和全局任务列表中都没有获取到任务的时候,就会执行trySteal
函数来偷取其他Worker的任务,来看看源码:
private fun trySteal(blockingOnly: Boolean): Task? {
assert { localQueue.size == 0 }
val created = createdWorkers
// 0 to await an initialization and 1 to avoid excess stealing on single-core machines
if (created < 2) {
return null
}
var currentIndex = nextInt(created)
var minDelay = Long.MAX_VALUE
repeat(created) {
++currentIndex
if (currentIndex > created) currentIndex = 1
val worker = workers[currentIndex]
if (worker !== null && worker !== this) {
assert { localQueue.size == 0 }
val stealResult = if (blockingOnly) {
localQueue.tryStealBlockingFrom(victim = worker.localQueue)
} else {
localQueue.tryStealFrom(victim = worker.localQueue)
}
if (stealResult == TASK_STOLEN) {
return localQueue.poll()
} else if (stealResult > 0) {
minDelay = min(minDelay, stealResult)
}
}
}
minDelayUntilStealableTaskNs = if (minDelay != Long.MAX_VALUE) minDelay else 0
return null
}
逻辑比较简单,通过随机数在全局的workers中选一个Worker,然后判断不是当前的Worker,最后在它的localQueue中偷一个任务来执行。这样就不会出现有的Worker闲着,而有的Worker中很多任务在等待执行的情况。
这里可以看到kotlin对协程的调度做了很多细节,充分的使用系统资源,保证了协程能得到及时处理。
这样整个分发和执行的流程大体上都讲完了,当然还有大量的细节没有提到,大家有兴趣自己去研究研究,包括这里面对于多线程间同步问题做了大量的工作。
Dispatchers.IO
最后来看看Dispatchers.IO的实现
@JvmStatic
public val IO: CoroutineDispatcher = DefaultIoScheduler
它是DefaultIoScheduler,里面有一个default
internal object DefaultIoScheduler : ExecutorCoroutineDispatcher(), Executor {
private val default = UnlimitedIoScheduler.limitedParallelism(
systemProp(
IO_PARALLELISM_PROPERTY_NAME,
64.coerceAtLeast(AVAILABLE_PROCESSORS)
)
)
...
}
实际上所有的工作都是这个default来完成的,那么它是什么,来看看limitedParallelism
的代码:
public open fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
parallelism.checkParallelism()
return LimitedDispatcher(this, parallelism)
}
最终返回的是一个LimitedDispatcher,它的第一个参数传入的是this,就是UnlimitedIoScheduler对象。
来看看LimitedDispatcher的dispatch函数:
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatchInternal(block) {
dispatcher.dispatch(this, this)
}
}
先通过dispatchInternal函数来进行内部处理,这个后面会详细说,然后还是由dispatcher,即UnlimitedIoScheduler来执行分发,看看它的源码
private object UnlimitedIoScheduler : CoroutineDispatcher() {
@InternalCoroutinesApi
override fun dispatchYield(context: CoroutineContext, block: Runnable) {
DefaultScheduler.dispatchWithContext(block, BlockingContext, true)
}
override fun dispatch(context: CoroutineContext, block: Runnable) {
DefaultScheduler.dispatchWithContext(block, BlockingContext, false)
}
}
可以看到,都是执行了DefaultScheduler的dispatchWithContext函数。前面我们知道Dispatchers.Default也是DefaultScheduler,所以分发流程是一样的。
所以Dispatchers.Default和Dispatchers.IO大体上是差不多的,线程池的各个参数也是一样的。不同的是这里执行dispatch的时候传入的都是BlockingContext,来标记这个任务是阻塞的。
dispatchInternal
但是Dispatchers.IO也有不同之处,可以看到limitedParallelism
这个函数有一个参数,传入的是一个int值:
systemProp(
IO_PARALLELISM_PROPERTY_NAME,
64.coerceAtLeast(AVAILABLE_PROCESSORS)
)
其实就是获取当前可用处理器的数量,那么这个值有什么用?
这个parallelism最终是传入到LimitedDispatcher中,前面提到它的dispatch函数首先会执行dispatchInternal,来看看做了什么:
private inline fun dispatchInternal(block: Runnable, dispatch: () -> Unit) {
if (addAndTryDispatching(block)) return
if (!tryAllocateWorker()) return
dispatch()
}
先执行了addAndTryDispatching,这个函数如下
private fun addAndTryDispatching(block: Runnable): Boolean {
queue.addLast(block)
return runningWorkers >= parallelism
}
可以看到先将这个任务加入队列中。
然后可以看到有一个runningWorkers的变量,就是当前正在运行的Worker的数量,由于是Dispatchers.IO所以就是当前正在运行阻塞任务(IO任务)的Worker数。
而parallelism会与他进行比较,当runningWorkers大于等于parallelism就不再继续分发;否则执行tryAllocateWorker,这个函数如下
private fun tryAllocateWorker(): Boolean {
synchronized(workerAllocationLock) {
if (runningWorkers >= parallelism) return false
++runningWorkers
return true
}
}
这里同样判断了当runningWorkers大于等于parallelism就返回false,从前面的代码看,返回false就不再继续分发了。当小于parallelism则会将runningWorkers加1并返回true,这样才会继续执行到DefaultScheduler里分发。
这就是Dispatchers.IO与Dispatchers.Default的一个不同之处,用一个parallelism来限制正在运行的Worker数量。
如果正在运行阻塞任务的Worker大于等于可用CPU数的话,就不会执行DefaultScheduler的分发,也就不会重新创建Worker,所以Dispatchers.IO中Worker的最大数量就是parallelism(当然还受maxPoolSize的限制)。
总结
通过上面可以看到Dispatchers.IO与Dispatchers.Default实际上使用的是一个线程池,corePoolSize是非阻塞Worker的核心数量,而阻塞Worker数量最大不超过parallelism,同时这两种Worker总数量不能超过maxPoolSize。
在分发方面,任务会先尝试放入本地队列,如果失败会放入全局队列,并且新建Worker进行处理。当一个Worker处理完自己的任务后会从全局队列获取任务处理,如果全局队列也空了就会尝试从其他Worker的队列中偷取任务来处理,这样就保证了每个任务都能尽快被执行。
在任务方面,分成了阻塞和非阻塞两种,并且有不同的处理逻辑,让非阻塞任务能够尽快的得到执行。