首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Kotlin协程-协程派发和调度框架

Kotlin协程-协程派发和调度框架

作者头像
PhoenixZheng
发布2021-04-26 12:26:44
9970
发布2021-04-26 12:26:44
举报

一个coroutine创建好之后,就交给协程框架去调度了。这篇主要讲从launch{...}开始,到最终得到执行的时候,所涉及到的协程框架内部概念。

一般开发中所接触到的协程类和接口无非是 launch, async, Dispatch.IO...,这些概念是对我们开发者来说的。进入协程源码的世界之后,这些概念就会被一些内部概念所替代。搞清楚内部概念对分析协程源码来说非常关键。

协程的最小粒度-Coroutine

对没接触过协程的人来说,一个OOP代码的最小调度粒度是函数。在协程中,最小的调度粒度是协程,在kotlin中叫coroutine。

下面是一段协程代码,

fun main() = runBlocking{
    launch{
        launch {
            // 在后台启动一个新的协程并继续r
            println("launch 3 > Thread: ${Thread.currentThread().name}")
            delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
            println("World!") // 在延迟后打印输出
        }
        println("launch 2 > Thread: ${Thread.currentThread().name}")
        delay(100L)
        println("Hello,") // 协程已在等待时主线程还在继续
        Thread.sleep(200L) // 阻塞主线程 2 秒钟来保证 JVM 存活
    }
    println("out launch done")
}

上面的代码中总共有三个coroutine。上面的代码的执行结果是,

out launch done launch 2 > Thread: main launch 3 > Thread: main Hello, World!

每个协程实际上是个调度单位。上面的执行顺序很有意思,在 "out launch done"结束之后,首先打印了"launch 2"。按理说接下来应该是“Hello”,但实际情况是“launch 3”。

因为协程在遇到挂起函数delay的时候,会把当前coroutine挂起,然后调度另外一个待执行的coroutine去执行。

在“launch 2”之后,协程遇到了另外一个delay,此时这个coroutine又会被挂起,然后切入之前那个coroutine。所以会看到“launch 2”之后不是“launch 3”,而是Hello。

这个例子是为了说明coroutine的调度原理。从这个角度看,可以理解“用同步代码写异步逻辑”的意思。对于开发者来说,上面的代码是按同步的思路写的。实际运行中,因为coroutine的调度,则变成了异步代码。

外部概念和内部概念

协程中外部概念和内部概念的差别很大。对应开发者来说,一个协程的最小粒度coroutine,在协程的内部概念中叫DispatchedContinuation。

Continuation是一个内部接口,它有一个对象和一个方法,

public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

这个接口的含义是,实现这个接口的类,具有可以暂停和继续的能力,也就是resumeWith()方法。

在协程源码里,所有的coroutine都会被封装成 DispatchedContinuation对象,但它实际上还是一个coroutine,只是增加了一些能力,这个我们后面会说。

另外还有很多内部概念,比较重要的是 Dispatcher 和 Scheduler。

Dispatcher

顾名思义这是个分发器类。它负责把 DispatchedContinuation 分发到不同的调度器中去执行。

Dispatcher的默认实现是一个 expect 类,

public expect object Dispatchers {
    public val Default: CoroutineDispatcher
    
    public val Main: MainCoroutineDispatcher
    
    public val Unconfined: CoroutineDispatcher
}

它提供了几个默认的派发逻辑,比如Default会在默认线程池中随机执行coroutine,Main会在主线程,比如Android中的UI线程里执行coroutine。还有Unconfined。

在jvm上,还多了个IO类型的默认实现。对于磁盘IO或者网络IO,一般用这个作为默认的实现。它对线程池有特殊的调度方式,可以保证计算密集型的coroutine效率不会受到IO coroutine的拖累。

Scheduler

调度器是协程的核心功能,所有的corotuine最后都在Scheduler中执行。

在默认情况下调度器的实现是 CoroutineScheduler,它会根据当前任务数量创建线程,把coroutine放到线程的自有队列和公有队列中。并且还实现了 ForkJoinPool 的核心思想 work-stealing。

private fun trySteal(blockingOnly: Boolean): Task? {
    ...
}

work-stealing的核心逻辑是,所有线程先去执行CPU密集型的coroutine。CPU密集型队列执行完之后,线程再去执行IO密集型coroutine。

最精彩的部分来了,当线程A的IO密集队列执行完毕,队列为空之后,它会去偷其他线程的IO任务队列。这是整个协程调度里最精彩的部分,work-stealing的设计,加上把CPU-bounded和IO-intensive任务区分出来,使得用了协程的代码效率得到极大的提升。

为什么可以提升效率,在Kotlin协程-协程设计的基础中有具体解释。

协程框架三大件,Continuation-Disptacher-Scheduler

Kotlin的协程从框架设计上就考虑了跨平台的问题。

这里的跨平台不是指安卓和服务端。而是指kotlin在支持 jvm / js/ native 三个平台上的跨平台。从它的设计上也能感受到kotlin想吊打其他语言的野心。俗话说“javascript抢别人的活”,现在看kotlin连js的活也想抢。不知道过个十年kotlin和js有哪个还能被开发者使用。

kotlinx-coroutine的代码框架这么分,

common --jvm --js --native

在kotlinx-coroutines-core中有个公有的 common 包。除此之外还有js,jvm,native三个包。

协程框架三大件在common里有通用实现,具体到每个平台上,还有真正的细节实现。

比如Dispatcher,在 common 包中是 Dispatchers.common.kt,

public expect object Dispatchers {
    public val Default: CoroutineDispatcher
    
    public val Main: MainCoroutineDispatcher
    
    public val Unconfined: CoroutineDispatcher
}

在jvm包中,是Dispatchers

public expect object Dispatchers {
    public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
    ...
}

可以看到jvm包中的方法名多了 actual修饰,后面也有具体的实现createDefaultDispatcher()。这是在java平台上真正用到的代码。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-04-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android每日一讲 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 协程的最小粒度-Coroutine
  • 外部概念和内部概念
    • Dispatcher
      • Scheduler
      • 协程框架三大件,Continuation-Disptacher-Scheduler
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档