深入理解Kotlin协程(二)

章节概览

本节 主要基于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

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享