深入理解Kotlin协程(二)

1,110 阅读5分钟

章节概览

本节 主要基于Kotlin协程的官方框架一些api的使用 来体会协程框架的设计思路,这个步骤 对你后面理解好Kotlin协程的框架源码 有很大帮助,千万不要省略

delay

delay 是每个协程使用者 使用最多的一个函数,但是如果让你来设计 应该如何做?

fun main(){

    suspend {
        println("${System.currentTimeMillis()}")
        delay(5000)
    }.startCoroutine(object :Continuation<Unit>{
        override val context: CoroutineContext
            get() = EmptyCoroutineContext

        override fun resumeWith(result: Result<Unit>) {
            println("${System.currentTimeMillis()}")
        }
    })
    //别让主线程直接结束了
    println("main thread:"+Thread.currentThread().id)
    Thread.sleep(10000)
}

suspend fun delay(time:Long,unit: TimeUnit =TimeUnit.MILLISECONDS){
    if (time<=0){
        return
    }
    val executor=Executors.newScheduledThreadPool(1){
        Thread(it,"Scheduler").apply {
            isDaemon=true
        }
    }
    suspendCoroutine<Unit> {continuation ->
        executor.schedule({
            // 执行完毕 可以回调了 把控制权交出去
            continuation.resume(Unit)
        },time,unit)
    }
}

验证下这个结果:

image.png

整体上其实不难的,甚至可以说简单, 本质上和我们在线程中调用Thread.sleep 是一个思路,只不过这里我们把sleep 放到了后台线程 当sleep 结束以后 通过类似于回调的机制 来告知 原来的线程 我等待结束了 你可以继续干活了。仅此而已。

为什么要这么设计?因为对于很多基于事件循环的ui系统来说。你在重要的诸如ui线程上sleep 是很容易造成系统卡顿的

这里大家要注意体会一下,本质上来说 上一篇文章 介绍的 一些协程api 就真的是仅仅属于协程api的!是在kotlin官方包之内的,但是 这些api 很难用,大家一般都会使用Kotlin的协程库! 也就是kotlinx-corotines-core 那几个包。下面主要是剖析一下 协程库的几个重要知识点。

启动模式

很多时候 我们拿到一个scope 都会直接lanuch 这是最方便的写法:

image.png

很多人可能都没注意到 start 这个参数 是可以配置的 而且有不同的配置效果

image.png

要搞清楚这个概念,首先要分清 立即调度立即执行的区别

所谓立即调度: 是指 调度器收到调度的指令,但是具体在什么时候 以及在哪个线程上执行 要根据调度器自己的情况决定

也就是说 通常而言 从立即调度状态到 立即执行状态 是有一段时间的

有了这个概念 我们再搞清楚这4个 start效果 就很容易了:

Default: 创建协程后 立即调度 调度前如果被取消,直接进入取消响应的状态 也就是说 有可能在执行前被取消

Atomic :创建协程后,立即调度,协程 执行到第一个挂起点之前 不响应取消 也就是说 协程一定会被执行(执行途中可能会被取消)

lazy:如果调度前被取消了,直接进入异常结束状态 且你不手动调用start等方法 他是不会执行的

通常而言,我们只要关注Default和Lazy 两种模式即可尤其是后者 lazy模式 在很多场景会很有用(想一下以前thread 线程开发时,是不是有很多时候 我们先定义了thread 然后在某种时机到来的时候再去start? )

调度器

协程的调度器 顾名思义就是调度你的协程 在哪个线程上执行的。 不用想的太高大上,背后都是线程池。 常用的 有 Default 默认调度器 一般适合后台计算任务 IO 调度器 自然是处理io相关的, 另外还有一个Main 一般就是ui线程的调度器 也就是android开发里面的所谓的主线程

来看下Default:

image.png

image.png

注意看 最终的Default调度器其实就是:

image.png

再来看看IO 调度器长啥样:

image.png

实际上 对于io调度器来说 会限制 对于io任务的并发量,因为过多的io任务并发 会占用过多的系统资源, 而default调度器则不会

既然对于调度器来说 有这个区别,但是在代码层面 是在哪里做的区分呢?

继续看源码,实际上所谓的调度 最终都是走的dispatch方法 、

image.png

image.png

最终会走到这个dispatchWithContext 这个方法里 image.png

最终就是在这个 CoroutineScheduler的dispatch 方法里 完成对 阻塞任务和非阻塞任务的 分发调度

image.png

协程一定可以被取消吗?

使用过一段时间协程的同学 可能会调用过cancel方法, 比如让一段正在执行的任务 退出,但是在某些场景下 这些任务 一旦开始可能不会退出,例如下面这个官方的扩展函数:

image.png

这个扩展函数真的很简单,主要就是提供一个拷贝流的操作,你就理解成是一个拷贝文件的操作就可以

我们来写一个测试demo:

fun main(){
   val job= GlobalScope.launch(start=CoroutineStart.LAZY){
        withContext(Dispatchers.IO){
            val inputStream=FileInputStream(File("/Users/wuyue/Downloads/office.pkg"))
            println("开始拷贝: ${System.currentTimeMillis()}")
            inputStream.copyTo(FileOutputStream(File("/Users/wuyue/Downloads/office2.pkg")))
            println("结束拷贝: ${System.currentTimeMillis()}")
        }
    }
    job.start()
    Thread.sleep(1000)
    job.cancel()
    println("job 已经 cancel: ${System.currentTimeMillis()}")
    //别让主线程直接结束了
    println("main thread:"+Thread.currentThread().id)
    Thread.sleep(10000)
}

我其实就是开了一个协程 拷贝一个文件 注意我这个文件很大 大概在1.8gb左右 所以拷贝时间 要3s左右 我在开始执行1s以后 执行了cancel方法

按道理我这个协程应该退出的 但是实际情况看日志:

image.png

这个时间线可以清晰的看出来,即使我们调用了cancel 方法 但是依然不会退出这个流拷贝的过程

那么有没有办法 可以解决这个问题呢?

suspend fun InputStream.copyToSuspend(out:OutputStream):Long{
    var bytesCopied: Long = 0
    val buffer = ByteArray(8 * 1024)
    var bytes = read(buffer)
    while (bytes >= 0) {
        yield()
        out.write(buffer, 0, bytes)
        bytesCopied += bytes
        bytes = read(buffer)
    }
    return bytesCopied
}

照葫芦画瓢 我们直接复制copy方法 只在里面 加入了一个yield函数的调用 这个函数 主要就是检查所在协程的状态,如果已经取消了 就直接退出来 让出执行权,给其他协程执行的机会

再次看执行结果:

image.png

符合预期!

有的时候 我们还可以指定 某个协程的执行环境 不要响应 cancel的操作(这点是不是很像 try catch的 finanllay的兜底操作?)

只要指定context 为NonCancellable 即可 image.png

另外还要注意的是,本质上来说 协程的cancel 能否成功 仅仅取决于你是否在你的协程内加入了检查点,比如 isActive 或者 上文介绍的yield, 如果你没有加入检查点,那你的cancel 一定是无效的, 这里一定要注意。