本文作者
作者:小鱼人爱编程
链接:
https://juejin.cn/post/7207078219215962170
本文由作者授权发布。
之前有分析过协程里的线程池的原理:Kotlin 协程之线程池探索之旅(与Java线程池PK),当时偏重于整体原理,对于细节之处并没有过多的着墨,后来在实际的使用过程中遇到了些问题,也引发了一些思考,故记录之。
https://juejin.cn/post/7114968347325759501
通过本篇文章,你将了解到:
为什么要设计Dispatchers.Default和Dispatchers.IO? Dispatchers.Default 是如何调度的? Dispatchers.IO 是如何调度的? 线程池是如何调度任务的? 据说Dispatchers.Default 任务会阻塞?该怎么办? 线程的生命周期是如何确定? 如何更改线程池的默认配置?
一则小故事
书接上篇:一个小故事讲明白进程、线程、Kotlin 协程到底啥关系?
https://juejin.cn/post/7108651566806073380
出场人物:
操作系统,简称OS
Java
Kotlin
private void startNewThread() {
new Thread(()->{
//线程体
//我在子线程执行...
}).start();
}
而Java也是按照此种方式创建线程执行任务。
某天,OS找到Java说到:"你最近的线程创建、销毁有点频繁,我这边切换线程的上下文是要做准备和善后工作的,有一定的代价,你看怎么优化一下?"
Java无辜地答到:"我也没办法啊,业务就是那么多,需要随时开启线程做支撑。"
OS不悦:"你最近态度有点消极啊,说到问题你都逃避,我理解你业务复杂,需要开线程,但没必要频繁开启关闭,甚至有些线程就执行了一会就关闭,而后又立马开启,这不是玩我吗?。这问题必须解决,不然你的KPI我没法打,你回去尽快想想给个方案出来。"
Java悻悻然:"好的,老大,我尽量。"
经过一段时间的优化,Java线程池框架已经比较稳定了,大家相安无事。
某天,OS又把Java叫到办公室:"你最近提交的任务都是很吃CPU,我就只有8个CPU,你核心线程数设置为20个,剩余的12个根本没机会执行,白白创建了它们。"
Java沉吟片刻道:"这个简单,针对计算密集型的任务,我把核心线程数设置为8就好了。"
OS略微思索:"也不失为一个办法,先试试吧,看看效果再说。"
过了几天,OS又召唤了Java,面带失望地道:"这次又是另一个问题了,最近提交的任务都不怎么吃CPU,基本都是IO操作,其它计算型任务又得不到机会执行,CPU天天在摸鱼。"
Java理所当然道:"是呀,因为设置的核心线程数是8,被IO操作的任务占用了,同样的方式对于这种类型任务把核心线程数提高一些,比如为CPU核数的2倍,变为16,这样即使其中一些任务占用了线程,还剩下其它线程可以执行任务,一举两得。"
OS来回踱步,思考片刻后大声道:"不对,你这么设置万一提交的任务都是计算密集型的咋办?又回到原点了,不妥不妥。"
Java似乎早料到OS有此疑问,无奈道:”没办法啊,我只有一个参数设置核心线程,线程池里本身不区分是计算密集型还是IO阻塞任务,鱼和熊掌不可兼得。"
OS怒火中烧,整准备拍桌子,在这关键时刻,办公室的门打开了,翩翩然进来的是Kotlin。
Kotlin看了Java一眼,对OS说到:"我已经知道两位大佬的担忧,食君俸禄,与君分忧,我这里刚好有一计策,解君燃眉之急。"
OS欣喜道:"小K,你有何妙计,速速道来。“
Kotlin平息了一下激动的内心:"我计策说起来很简单,在提交任务的时候指定其是属于哪种类型的任务,比如是计算型任务,则选择Dispatchers.Default,若是IO型任务则选择Dispatchers.IO,这样调用者就不用关注其它的细节了。"
Java说到:"这策略我不是没有想到,只是担忧越灵活可能越不稳定。"
OS打断他说:"先让小K完整说一下实现过程,下来你俩仔细对一下方案,扬长避短,吃一堑长一智,这次务必要充分考虑到各种边界情况。"
Java&Kotlin:"好的,我们下来排期。"
故事讲完,言归正传。
GlobalScope.launch(Dispatchers.Default) {
println("我是计算密集型任务")
}
Dispatchers.Default 原理
概念约定
GlobalScope.launch(Dispatchers.Default) {
println("我是计算密集型任务")
Thread.sleep(20000000)
}
在任务里执行线程的睡眠操作,此时虽然线程处于挂起状态,但它还没执行完任务,在线程池里的状态我们认为是忙碌的。
再看如下代码:
GlobalScope.launch(Dispatchers.Default) {
println("我是计算密集型任务")
Thread.sleep(2000)
println("任务执行结束")
}
调度原理
1. launch(Dispatchers.Default) 作用是创建任务加入到线程池里,并尝试通知线程池里的线程执行任务。
2. launch(Dispatchers.Default) 执行并不耗时。
线程池是否有空闲的线程。 创建新线程是否成功。
#CoroutineScheduler
private fun tryCreateWorker(state: Long = controlState.value): Boolean {
//线程池已经创建并且还在存活的线程总数
val created = createdWorkers(state)
//当前IO类型的任务数
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
}
1. Dispatchers.Default 提交任务,此时线程池里所有任务都在忙碌,于是尝试创建新的线程,而又因为当前计算型的线程数=8,等于核心线程数,此时不能创建新的线程,因此该任务暂时无法被线程执行。 2. Dispatchers.IO 提交任务,此时线程池里所有任务都在忙碌,于是尝试创建新的线程,而当前阻塞的任务数为1,当前线程池所有线程个数为8,因此计算型的线程数为 8-1=7,小于核心线程数,最后可以创建新的线程用以执行任务。
这也是两者的最大差异,因为对于计算型(非阻塞)的任务,很占CPU,即使分配再多的线程,CPU没有空闲去执行这些线程也是白搭,而对于IO型(阻塞)的任务,不怎么占CPU,因此可以多开几个线程充分利用CPU性能。
1. 只有获得cpu许可的线程才能执行计算型任务,而cpu许可的个数就是核心线程数。 2. 如果线程没有找到可执行的任务,那么线程将会进入挂起状态,此时线程即为空闲状态。 3. 当线程再次被唤醒后,会判断是否已经被终止,若是则退出,此时线程就销毁了。
1. 线程挂起的时间到了。 2. 挂起的过程中,有新的任务加入到线程池里,此时将会唤醒线程。
binding.btnStartThreadMultiCpu.setOnClickListener {
repeat(8) {
GlobalScope.launch(Dispatchers.Default) {
println("cpu multi...${multiCpuCount++}")
Thread.sleep(36000000)
}
}
}
var singleCpuCount = 1
binding.btnStartThreadSingleCpu.setOnClickListener {
repeat(1) {
GlobalScope.launch(Dispatchers.Default) {
println("cpu single...${singleCpuCount++}")
Thread.sleep(36000000)
}
}
}
var singleIoCount = 1
binding.btnStartThreadSingleIo.setOnClickListener {
repeat(1) {
GlobalScope.launch(Dispatchers.IO) {
println("io single...${singleIoCount++}")
Thread.sleep(10000)
}
}
}
1. 计算密集型任务能分配的最大线程数为核心的线程数(默认为CPU核心个数,比如我们的实验设备上是8个),若之前的核心线程数都处在忙碌,新开的任务将无法得到执行。 2. IO型任务能开的线程默认为64个,只要没有超过64个并且没有空闲的线程,那么就一直可以开辟新线程执行新任务。
这也给了我们一个启示:Dispatchers.Default 不要用来执行阻塞的任务,它适用于执行快速的、计算密集型的任务,比如循环、又比如计算Bitmap等。
线程挂起时设定了挂起的结束时间点,当线程唤醒后检查当前时间有没有达到结束时间点,若没有,则说明被新加入的任务动作唤醒的。
即使是没有了任务执行,若是当前线程数小于核心线程数,那么也无需销毁线程,继续等待任务的到来即可。
internal val CORE_POOL_SIZE = systemProp(
//从这个属性里取值
"kotlinx.coroutines.scheduler.core.pool.size",
AVAILABLE_PROCESSORS.coerceAtLeast(2),//默认为cpu的个数
minValue = CoroutineScheduler.MIN_SUPPORTED_POOL_SIZE//最小值为1
)
System.setProperty("kotlinx.coroutines.scheduler.core.pool.size", "20")
System.setProperty("kotlinx.coroutines.io.parallelism", "40")
本文基于Kotlin 1.5.3,文中完整实验Demo请点击。
https://github.com/fishforest/KotlinDemo/blob/master/app/src/main/java/com/fish/kotlindemo/lifecycleAndCoroutine/ThirdActivity.kt
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!