day11 Kotlin coroutine 花生什麽事?

前面我讲10篇了,告诉你们coroutine是什麽,怎麽用,如何切thread,和她背後发生什麽事

其实有人要我写那些内建的suspend function,但我觉得没必要,你要是把基本概念搞懂了,那些内建的挂起函数,大多看一下描述就会用了,甚至不用看,比如说delay(),但如果你基础没搞懂,你用了也是懵懵懂懂的用,到了debug的时候,终究还是得把基础搞懂

这篇,我会就前面10篇的所有内容,解答一些疑问,也会给出几个例子讲解,可以搭配之前的文章阅读

thread的挂起和coroutine的挂起

首先要了解coroutine和thread是不同的东西,os不知道coroutine,而coroutine会把任务透过handler.post加入thread里面我在这里讲过

那如果今天的任务是suspend的呢

suspend fun doTask(){
    withContext(Dispatcher.IO){
        val apiResult = apiCall()
        writeToRoom(apiResult)
    }
}
suspend fun apiCall(){

}
suspend fun writeToRoom(){

}

会变成

mHandler.post{
    doTask()
}

当Main thread执行到doTask时,会将任务切换到 IO thread执行,并在Main Thread终止doTask的coroutine,这样Main thread就能继续绘制ui,画面也就不卡了

但在coruotine里面的suspend就效果就不太一样,apiCall和writeToRoom都是挂起函数,他们透过finite machine会被编译成state machine,递回执行,并透过class共享参数,这边看suspend编译
而站在Thread调用者的角度,就是函数可被挂起,但状态保留,并在耗时任务结束後返回值

并发和挂起,冲突吗?还是完美情侣?

网上有种说法,coroutine比较高效,因为他能把api呼叫挂起,不用傻等,这样thread就能去执行下一个api call,对吗?

错!!!
对thread来说,挂起是将任务切换到别的thread执行,对coroutine来说,挂起是编译後照顺序执行,任务每个都要做,而且得照顺序做,并不会比较高效
而实际使用上,在main thread呼叫suspend function是很常见的,而我们通常用withContext确保main thread safe

lifecycleScope.launch {
    callDelay()
    callDelay()
    callDelay()
}
suspend fun callDelay(){
    withContext(Dispatchers.IO){
        delay(5000L)
        Timber.d(Thread.currentThread().name)
    }
}

2021-09-26 14:15:32.511 9530-9587/: DefaultDispatcher-worker-1
2021-09-26 14:15:37.516 9530-9587/: DefaultDispatcher-worker-1
2021-09-26 14:15:42.521 9530-9587/: DefaultDispatcher-worker-1

那并发呢?用并发加挂起不就可以不用傻等?thread的利用不就高效了?

还是错!!并发执行任务可以减少等待的时间,但非常重要的一点,async会做什麽?
他会创造一个新的子coroutine,对吧?
那实际上在干嘛,是由多个子couroutine在多个io thread同时做多个任务,不是他们在同一个io thread工作

宝杰,那这个例子,你怎麽看?

lifecycleScope.launch {
    val a = async (Dispatchers.Main){
        delay(2000L)
        Timber.d("a")
    }
    val b = async (Dispatchers.Main){
        delay(2000L)
        Timber.d("b")
    }
    val c = async (Dispatchers.Main) {
        delay(2000L)
        Timber.d("c")
    }
    awaitAll(a,b,c)
}
//2021-09-26 14:22:12.109 $a: a
//2021-09-26 14:22:12.111 $b: b
//2021-09-26 14:22:12.114 $c: c

我不是利用suspend高效的使用main thread了吗?

并不是,delay()的底层是用java.Executor完成的
An {@code Executor} is normally used instead of explicitly creating threads.

source: kotlin.coroutines.Executors.kt
这边就能连到java.Exectutor了

白话来说是什麽意思呢?
阻塞的东西被放到Exectutor执行了,结束後会再切回Main thread; 另一方面,这个代码并不能表达所谓的高效,我们所谓的阻塞代码应该是Thread.sleep(),而这样就只会照顺序进行了

cpu核心数量有限,为什麽可以并发那麽多

首先要了解cpu的运作速度,在pixel2上,单个cpu周期低於0.0000000004秒,一般人眨言大约花0.4秒,也就是说,你眨眼的时间cpu能运行10亿个周期,还不算现在已经要出pixel6(好想要pixel6呀)
资料来源

这跟并发有关系吗?大有关系

当线程处于IO操作时,线程是阻塞的,线程由运行状态切换到等待状态。此时CPU会做上下文切换,以便处理其他程序;当IO操作完成后,CPU会收到一个来自硬盘的中断信号,CPU正在执行的线程因此会被打断,回到ready队列。而先前因I/O而waiting的线程随着I/O的完成也再次回到就绪队列,此时CPU可能会选择他执行。
转自腾讯云

thread能再利用吗?

看情况,thread在任务执行完後,有其他任务就会继续执行其他任务,没有的话,就会被回收

scope取消了,那工作还做吗?

fun testScope(){
    scope.launch {
        while (true){
            //work
        }
    }
}

那呼叫cancel後,工作还会做吗?
会,记得吗?coroutine会等待的工作结束後才返回,那要怎麽办呢?
让他每次执行前都检查isActive,这边能直接改成while(isActive)
executor

稍微聊聊dispatcher

Dispatcher.Main - 因为篇幅跟新手取向的关系,这部分我也就不讲了,只讲结果,耗时工作如果跑在main thread,画面会freeze,并发生ANR(application not response), 因为main thread会负责绘制ui

Dispatcher.IO - I/O thread顾名思义,我们大多用来读写操作,从资料库抓资料、api资料等等,可以很多很多个

Dispatcher.Default - 之前提过Default的use case是 大量运用cpu资源运算的工作,如资料排序或解析json,DiffUtil等等,但Default和IO有着不小的关系,必须了解一下,所以又翻了文档

The default CoroutineDispatcher that is used by all standard builders like launch, async, etc if neither a dispatcher nor any other ContinuationInterceptor is specified in their context.

It is backed by a shared pool of threads on JVM. By default, the maximum number of threads used by this dispatcher is equal to the number of CPU cores, but is at least two.

虽然英文不长,简单说就是launch/ async建构子预设是走default, 数量最多和CPU核心数量一样多,最少两个,那跟IO也没关系呀,有的,要去看source code,这边我就不带了,请看jast的文章,这边节录重点 IO 是基於 Default 去建立的,IO 与 Default 的差别在於 Default 开的 thread 会受到 CPU core 限制,而 IO 则会比 Default 多上不少。

Dispatcher.Unconfined - 除了yield以外,原本在哪个thread就跑在哪个thread,这个特性很重要 文档,也有例外(yield)在jast那篇

我是谁我在哪? suspend和withContext切换disatcher

第一关

lifecycleScope.launch {
    Timber.d( "${Thread.currentThread().name}" )
    withContext(Dispatchers.Default){
        Timber.d( "${Thread.currentThread().name}" )
    }
    withContext(Dispatchers.Main){
        Timber.d( "${Thread.currentThread().name}" )
    }
    withContext(Dispatchers.IO){
        Timber.d( "${Thread.currentThread().name}" )
    }
    withContext(Dispatchers.Unconfined){
        Timber.d( "${Thread.currentThread().name}" )
    }
}

/**
 * main
 * DefaultDispatcher-worker-1
 * main
 * DefaultDispatcher-worker-1
 * main
 * */

记得吧,unConfined通常会跑在正在跑的Thread,以及io和default的关系

第二关

lifecycleScope.launch {
    Timber.d( "${Thread.currentThread().name}" )

    withContext(Dispatchers.Default){
        launch (Dispatchers.Unconfined){
            Timber.d( "${Thread.currentThread().name}" )
            withContext(Dispatchers.Main){
                Timber.d( "${Thread.currentThread().name}" )
            }
            Timber.d( "${Thread.currentThread().name}" )
        }
    }

}

/**
 * main //lifecycle
 * DefaultDispatcher-worker-1 //Unconfined
 * main //Main
 * main //Unconfined
 * */

解释一下,第二个Unconfined会在main thread的原因之前讲过了,withContext结束後,会invoke Continuation(Unconfined),但因为他本身特性,所以会直接跑在Main thread

但如果今天我们把withContext换成launch,launch是创造了child coroutine,而不是切换thread,所以会变成

withContext(Dispatchers.Default){
    launch (Dispatchers.Unconfined){
        Timber.d( "${Thread.currentThread().name}" )
        launch (Dispatchers.Main){
            Timber.d( "${Thread.currentThread().name}" )
        }
        Timber.d( "${Thread.currentThread().name}" )
    }
}
/**
 * main //lifecycle
 * DefaultDispatcher-worker-1 //Unconfined
 * main //Main
 * DefaultDispatcher-worker-1 //Unconfined
 * */

到这边,大家应该都懂了thread会怎麽切换了吧

连结统整

必看

jast的文章
Coroutine context and dispatchers

选看

转自腾讯云
executor


<<:  Day 13 [Python ML、Pandas] 创建、读取和写入

>>:  第11车厢-table界的神器!DataTables介绍篇(1)

[Day04]K8s Cluster环境-安装在本地端

什麽是 Minikube ? Minikube 是一个轻量级工具,可以看做是只有单一节点的 Kube...

【Day 20】 实作 - 於 AWS Quicksight 建立 Sankey diagram 以及设定 Action

昨天我们已经透过 AWS Glue Job 来调整 Partition 分区结构以及将此格式转换成 ...

【把玩Azure DevOps】Day7 CI/CD从这里:设定第一个Pipeline(范本与编辑介面介绍)

前一篇介绍了要用来作为建立Pipeline的材料,这篇就要开始来建立第一个Build Pipelin...

Spring Framework X Kotlin Day 6 Unit Test

GitHub Repo https://github.com/b2etw/Spring-Kotlin...

DAY14 资料室--Vuex项目结构

前言 Vuex 并不会限制我们的代码结构,只是有三大原则需要遵守: 应用层级的状态应该集中到单个 s...