深圳网站建设的基,怎么做微信小程序,抚顺网站建设,怎样用xampp做网站前言
之前有分析过协程里的线程池的原理#xff1a;Kotlin 协程之线程池探索之旅(与Java线程池PK)#xff0c;当时偏重于整体原理#xff0c;对于细节之处并没有过多的着墨#xff0c;后来在实际的使用过程中遇到了些问题#xff0c;也引发了一些思考#xff0c;故记录之…前言
之前有分析过协程里的线程池的原理Kotlin 协程之线程池探索之旅(与Java线程池PK)当时偏重于整体原理对于细节之处并没有过多的着墨后来在实际的使用过程中遇到了些问题也引发了一些思考故记录之。 通过本篇文章你将了解到 为什么要设计Dispatchers.Default和Dispatchers.IODispatchers.Default 是如何调度的Dispatchers.IO 是如何调度的线程池是如何调度任务的据说Dispatchers.Default 任务会阻塞该怎么办线程的生命周期是如何确定如何更改线程池的默认配置 1. 为什么要设计Dispatchers.Default和Dispatchers.IO
一则小故事
书接上篇一个小故事讲明白进程、线程、Kotlin 协程到底啥关系 出场人物 操作系统简称OS Java Kotlin 在Java的世界里支持多线程编程开启一个线程的方式很简单 private void startNewThread() {new Thread(()-{//线程体//我在子线程执行...}).start();}而Java也是按照此种方式创建线程执行任务。 某天OS找到Java说到“你最近的线程创建、销毁有点频繁我这边切换线程的上下文是要做准备和善后工作的有一定的代价你看怎么优化一下” Java无辜地答到“我也没办法啊业务就是那么多需要随时开启线程做支撑。” OS不悦“你最近态度有点消极啊说到问题你都逃避我理解你业务复杂需要开线程但没必要频繁开启关闭甚至有些线程就执行了一会就关闭而后又立马开启这不是玩我吗。这问题必须解决不然你的KPI我没法打你回去尽快想想给个方案出来。” Java悻悻然“好的老大我尽量。”
Java果然不愧是编程界的老手很快就想到了方案他兴冲冲地找到OS汇报“我想到了一个绝佳的方案建立一个线程池固定开启几个线程有任务的时候往线程池里的任务队列扔就完事了线程池会找到已提交的任务进行执行。当执行完单个任务之后线程继续查找任务队列如果没有任务执行的话就睡眠等待等有任务过来的时候通知线程起来继续干活这样一来就不用频繁创建与销毁线程了perfect”
OS抚掌夸赞“池化技术这才是我认识的Java嘛不过线程也无需一直存活吧” Java“这块我早有应对之策线程池可以提供给外部接口用来控制线程空闲的时间如果超过这时间没有任务执行那就辞退它销毁我们不养闲人” OS满意点点头“该方案我准了细节之处你再完善一下。”
经过一段时间的优化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完整说一下实现过程下来你俩仔细对一下方案扬长避短吃一堑长一智这次务必要充分考虑到各种边界情况。” JavaKotlin“好的我们下来排期。”
故事讲完言归正传。
2. Dispatchers.Default 是如何调度的
Dispatchers.Default 使用 GlobalScope.launch(Dispatchers.Default) {println(我是计算密集型任务)}开启协程指定其运行的任务类型为Dispatchers.Default。 此时launch函数闭包里的代码将在线程池里执行。 Dispatchers.Default 用在计算密集型的任务场景里此种任务比较吃CPU。
Dispatchers.Default 原理
概念约定
在解析原理之前先约定一个概念如下代码 GlobalScope.launch(Dispatchers.Default) {println(我是计算密集型任务)Thread.sleep(20000000)}在任务里执行线程的睡眠操作此时虽然线程处于挂起状态但它还没执行完任务在线程池里的状态我们认为是忙碌的。 再看如下代码 GlobalScope.launch(Dispatchers.Default) {println(我是计算密集型任务)Thread.sleep(2000)println(任务执行结束)}当任务执行结束后线程继续查找任务队列的任务若没有任务可执行则进行挂起操作在线程池里的状态我们认为是空闲的。
调度原理 注此处忽略了本地队列的场景 由上图可知 launch(Dispatchers.Default) 作用是创建任务加入到线程池里并尝试通知线程池里的线程执行任务launch(Dispatchers.Default) 执行并不耗时 3. Dispatchers.IO 是如何调度的
直接看图
很明显地看出和Dispatchers.Default的调度很相似其中标蓝的流程是重点的差异之处。
结合Dispatchers.Default和Dispatchers.IO调度流程可知影响任务执行的步骤有两个 线程池是否有空闲的线程创建新线程是否成功 我们先分析第2点从源码里寻找答案 #CoroutineSchedulerprivate 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}怎么去理解以上代码的逻辑呢举个例子 假设核心线程数为8初始时创建了8个Default线程并一直保持忙碌。 此时分别使用Dispatchers.Default 和 Dispatchers.IO提交任务看看有什么效果。 Dispatchers.Default 提交任务此时线程池里所有任务都在忙碌于是尝试创建新的线程而又因为当前计算型的线程数8等于核心线程数此时不能创建新的线程因此该任务暂时无法被线程执行Dispatchers.IO 提交任务此时线程池里所有任务都在忙碌于是尝试创建新的线程而当前阻塞的任务数为1当前线程池所有线程个数为8因此计算型的线程数为 8-17小于核心线程数最后可以创建新的线程用以执行任务 这也是两者的最大差异因为对于计算型(非阻塞)的任务很占CPU即使分配再多的线程CPU没有空闲去执行这些线程也是白搭而对于IO型(阻塞)的任务不怎么占CPU因此可以多开几个线程充分利用CPU性能。
4. 线程池是如何调度任务的
不论是launch(Dispatchers.Default) 还是launch(Dispatchers.IO) 它们的目的是将任务加入到队列并尝试唤醒线程或是创建新的线程而线程寻找并执行任务的功能并不是它们完成的这就涉及到线程池调度任务的功能。
线程池里的每个线程都会经历上图流程我们很容易得出结论 只有获得cpu许可的线程才能执行计算型任务而cpu许可的个数就是核心线程数如果线程没有找到可执行的任务那么线程将会进入挂起状态此时线程即为空闲状态当线程再次被唤醒后会判断是否已经被终止若是则退出此时线程就销毁了 处在空闲状态的线程被唤醒有两种可能 线程挂起的时间到了挂起的过程中有新的任务加入到线程池里此时将会唤醒线程 5. 据说Dispatchers.Default 任务会阻塞该怎么办
在了解了线程池的任务分发与调度之后我们对线程池的核心功能有了一个比较全面的认识。 接着来看看实际的应用先看Demo 假设我们的设备有8核。 先开启8个计算型任务 binding.btnStartThreadMultiCpu.setOnClickListener {repeat(8) {GlobalScope.launch(Dispatchers.Default) {println(cpu multi...${multiCpuCount})Thread.sleep(36000000)}}}每个任务里线程睡眠了很长时间。
从打印可以看出8个任务都得到了执行且都在不同的线程里执行。
此时再次开启一个计算型任务 var singleCpuCount 1binding.btnStartThreadSingleCpu.setOnClickListener {repeat(1) {GlobalScope.launch(Dispatchers.Default) {println(cpu single...${singleCpuCount})Thread.sleep(36000000)}}}先猜测一下结果 答案是没有任何打印新加入的任务没有得到执行。
既然计算型任务无法得到执行那我们尝试换为IO任务 var singleIoCount 1binding.btnStartThreadSingleIo.setOnClickListener {repeat(1) {GlobalScope.launch(Dispatchers.IO) {println(io single...${singleIoCount})Thread.sleep(10000)}}}这次有打印了说明IO任务得到了执行并且是新开的线程。
这是为什么呢 计算密集型任务能分配的最大线程数为核心的线程数默认为CPU核心个数比如我们的实验设备上是8个若之前的核心线程数都处在忙碌新开的任务将无法得到执行IO型任务能开的线程默认为64个只要没有超过64个并且没有空闲的线程那么就一直可以开辟新线程执行新任务 这也给了我们一个启示Dispatchers.Default 不要用来执行阻塞的任务它适用于执行快速的、计算密集型的任务比如循环、又比如计算Bitmap等。
6. 线程的生命周期是如何确定
是什么决定了线程能够挂起又是什么决定了它唤醒后的动作 先从挂起说起当线程发现没有任务可执行后它会经历如下步骤 重点在于线程被唤醒后确定是哪种场景下被唤醒的判断方式也很简单 线程挂起时设定了挂起的结束时间点当线程唤醒后检查当前时间有没有达到结束时间点若没有则说明被新加入的任务动作唤醒的 即使是没有了任务执行若是当前线程数小于核心线程数那么也无需销毁线程继续等待任务的到来即可。
7. 如何更改线程池的默认配置
上面几个小结涉及到核心线程数线程挂起时间最大线程数等这些参数在Java提供的线程池里都可以动态配置灵活度很高而Kotlin里的线程池比较封闭没有提供额外的接口进行配置。 不过好在我们可以通过设置系统参数来解决这问题。
比如你可能觉得核心线程数为cpu的个数配置太少了想增加这数量这想法完全是可以实现的。 先看核心线程数从哪获取的。
internal val CORE_POOL_SIZE systemProp(//从这个属性里取值kotlinx.coroutines.scheduler.core.pool.size,AVAILABLE_PROCESSORS.coerceAtLeast(2),//默认为cpu的个数minValue CoroutineScheduler.MIN_SUPPORTED_POOL_SIZE//最小值为1
)若是我们没有设置kotlinx.coroutines.scheduler.core.pool.size属性那么将取到默认值比如现在大部分是8核cpu那么CORE_POOL_SIZE8。
若要修改则在线程池启动之前设置属性值 System.setProperty(kotlinx.coroutines.scheduler.core.pool.size, 20)设置为20此时我们再按照第5小结的Demo进行测试就会发现Dispatchers.Default 任务不会阻塞。
当然你觉得IO任务配置的线程数太多了默认64想要降低则修改属性如下 System.setProperty(kotlinx.coroutines.io.parallelism, 40)其它参数也可依此定制不过若没有强烈的意愿建议遵守默认配置。
通过以上的7个问题的分析与解释相比大家都比较了解线程池的原理以及使用了那么赶紧使用Kotlin线程池来规范线程的使用吧使用得当可以提升程序运行效率减少OOM发生。
本文基于Kotlin 1.5.3文中完整实验Demo请点击
您若喜欢请点赞、关注、收藏您的鼓励是我前进的动力
持续更新中和我一起步步为营系统、深入学习Android/Kotlin 1、Android各种Context的前世今生 2、Android DecorView 必知必会 3、Window/WindowManager 不可不知之事 4、View Measure/Layout/Draw 真明白了 5、Android事件分发全套服务 6、Android invalidate/postInvalidate/requestLayout 彻底厘清 7、Android Window 如何确定大小/onMeasure()多次执行原因 8、Android事件驱动Handler-Message-Looper解析 9、Android 键盘一招搞定 10、Android 各种坐标彻底明了 11、Android Activity/Window/View 的background 12、Android Activity创建到View的显示过 13、Android IPC 系列 14、Android 存储系列 15、Java 并发系列不再疑惑 16、Java 线程池系列 17、Android Jetpack 前置基础系列 18、Android Jetpack 易学易懂系列 19、Kotlin 轻松入门系列 20、Kotlin 协程系列全面解读