章节概览
本节 主要基于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)
}
}
验证下这个结果:
整体上其实不难的,甚至可以说简单, 本质上和我们在线程中调用Thread.sleep 是一个思路,只不过这里我们把sleep 放到了后台线程 当sleep 结束以后 通过类似于回调的机制 来告知 原来的线程 我等待结束了 你可以继续干活了。仅此而已。
为什么要这么设计?因为对于很多基于事件循环的ui系统来说。你在重要的诸如ui线程上sleep 是很容易造成系统卡顿的
这里大家要注意体会一下,本质上来说 上一篇文章 介绍的 一些协程api 就真的是仅仅属于协程api的!是在kotlin官方包之内的,但是 这些api 很难用,大家一般都会使用Kotlin的协程库! 也就是kotlinx-corotines-core 那几个包。下面主要是剖析一下 协程库的几个重要知识点。
启动模式
很多时候 我们拿到一个scope 都会直接lanuch 这是最方便的写法:
很多人可能都没注意到 start 这个参数 是可以配置的 而且有不同的配置效果
要搞清楚这个概念,首先要分清 立即调度和立即执行的区别
所谓立即调度: 是指 调度器收到调度的指令,但是具体在什么时候 以及在哪个线程上执行 要根据调度器自己的情况决定
也就是说 通常而言 从立即调度状态到 立即执行状态 是有一段时间的
有了这个概念 我们再搞清楚这4个 start效果 就很容易了:
Default: 创建协程后 立即调度 调度前如果被取消,直接进入取消响应的状态 也就是说 有可能在执行前被取消
Atomic :创建协程后,立即调度,协程 执行到第一个挂起点之前 不响应取消 也就是说 协程一定会被执行(执行途中可能会被取消)
lazy:如果调度前被取消了,直接进入异常结束状态 且你不手动调用start等方法 他是不会执行的
通常而言,我们只要关注Default和Lazy 两种模式即可, 尤其是后者 lazy模式 在很多场景会很有用(想一下以前thread 线程开发时,是不是有很多时候 我们先定义了thread 然后在某种时机到来的时候再去start? )
调度器
协程的调度器 顾名思义就是调度你的协程 在哪个线程上执行的。 不用想的太高大上,背后都是线程池。 常用的 有 Default 默认调度器 一般适合后台计算任务 IO 调度器 自然是处理io相关的, 另外还有一个Main 一般就是ui线程的调度器 也就是android开发里面的所谓的主线程
来看下Default:
注意看 最终的Default调度器其实就是:
再来看看IO 调度器长啥样:
实际上 对于io调度器来说 会限制 对于io任务的并发量,因为过多的io任务并发 会占用过多的系统资源, 而default调度器则不会。
既然对于调度器来说 有这个区别,但是在代码层面 是在哪里做的区分呢?
继续看源码,实际上所谓的调度 最终都是走的dispatch方法 、
最终会走到这个dispatchWithContext 这个方法里
最终就是在这个 CoroutineScheduler的dispatch 方法里 完成对 阻塞任务和非阻塞任务的 分发调度
协程一定可以被取消吗?
使用过一段时间协程的同学 可能会调用过cancel方法, 比如让一段正在执行的任务 退出,但是在某些场景下 这些任务 一旦开始可能不会退出,例如下面这个官方的扩展函数:
这个扩展函数真的很简单,主要就是提供一个拷贝流的操作,你就理解成是一个拷贝文件的操作就可以
我们来写一个测试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方法
按道理我这个协程应该退出的 但是实际情况看日志:
这个时间线可以清晰的看出来,即使我们调用了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函数的调用 这个函数 主要就是检查所在协程的状态,如果已经取消了 就直接退出来 让出执行权,给其他协程执行的机会
再次看执行结果:
符合预期!
有的时候 我们还可以指定 某个协程的执行环境 不要响应 cancel的操作(这点是不是很像 try catch的 finanllay的兜底操作?)
只要指定context 为NonCancellable 即可
另外还要注意的是,本质上来说 协程的cancel 能否成功 仅仅取决于你是否在你的协程内加入了检查点,比如 isActive 或者 上文介绍的yield, 如果你没有加入检查点,那你的cancel 一定是无效的, 这里一定要注意。