Kotlin协程系列(一)

一.协程的定义

最近看了一本有关kotlin协程的书籍,对协程又有了不一样的了解,所以准备写一个关于kotlin协程系列的文章。

言归正传,我们在学习一个新东西的时候,如果连这个东西"是什么"都回答不了,那么自然很难进入知识获取阶段的"为什么"和"怎么办"这两个后续环节了。因此,我们首先得知道协程的定义。

协程的概念最核心的点就是一段程序能够被挂起,稍后再在挂起的位置恢复。并且,挂起和恢复是开发者的程序逻辑自己控制的,协程是通过主动挂起让出运行权来实现协作的,因此它本质上是在讨论程序控制流程的机制,这是最核心的点,任何场景下讨论协程都能落脚到挂起和恢复。

二.协程和线程的联系和区别

联系:协程和线程都可以实现并发性,允许程序在同一时间处理多个任务;协程和线程都可以用于异步编程。

区别:协程是一种轻量级的线程,运行在线程之上。相比线程,协程消耗的资源较少,因为每个线程都有自己的堆栈和上下文,而协程是运行在用户空间的,不需要保存上下文信息。线程在等待某种资源或者等待I/O操作完成时,会被阻塞,并且在阻塞的期间还一直霸占着CPU资源。而协程的挂起是不会阻塞线程的,运行在这个线程上的其他协程还会照常执行,并且协程挂起时会主动释放自己的CPU资源。

三.Kotlin协程的基础设施

Kotlin的协程实现分为两个层次:

  • 基础设施层:标准库的协程API,主要对协程提供了概念和语义上最基本的支持
  • 业务框架层:协程的上层框架支持,也就是在基础设施层的基础上再封装一层

为了便于区分,我们将Kotlin协程的基础设施层创建的协程称为简单协程,将基于业务框架层创建的协程称为复合协程,这一小节主要来讨论简单协程的使用。

(1)简单协程的创建

复制代码
val continuation=suspend{
        println("协程体内")
        "Hello Coroutine"
    }.createCoroutine(object:Continuation<String>{
        override val context: CoroutineContext
            get() = EmptyCoroutineContext
        override fun resumeWith(result: Result<String>) {//协程挂起后,在恢复执行时会执行该函数
            println("协程运行结束,结果为:$result")
        }
    })

标准库提供了一个createCoroutine函数,我们可以使用它来创建协程,但是创建的协程不会立马执行。我们先来看看它的声明:

复制代码
public fun <T> (suspend () -> T).createCoroutine(
    completion: Continuation<T>
): Continuation<Unit>

其中suspend ()->T是一个被suspend修饰的挂起函数,这也是协程的执行体,不妨称作协程体

参数completion会在协程执行完成后调用,也就是协程的完成回调

函数的返回值是一个Continuation对象,其实也是指我们的协程体,只是套上了一层壳,协程挂起后的恢复执行,就是由它负责的

(2)协程的启动

调用continuation.resume(Unit)之后,协程体会立即执行。

不过一般来说,协程创建完成之后,我们会要求它立马执行,因此标准库也提供了一个一步到位的函数:

复制代码
public fun <T> (suspend () -> T).startCoroutine(
    completion: Continuation<T>
) {
    createCoroutineUnintercepted(completion).intercepted().resume(Unit)
}

启动上面创建的协程,会得到返回值:

{协程体内

协程运行结束,结果为:Success(Hello Coroutine)}

也就是说,协程体的返回值会作为resumeWith的参数传入,如本例中就得到Success(Hello Coroutine)

(3)协程体的Receiver

协程的创建和启动一共有两组api,另一组api如下:

复制代码
public fun <R, T> (suspend R.() -> T).createCoroutine(
    receiver: R,
    completion: Continuation<T>
): Continuation<Unit>

public fun <R, T> (suspend R.() -> T).startCoroutine(
    receiver: R,
    completion: Continuation<T>
) {
    createCoroutineUnintercepted(receiver, completion).intercepted().resume(Unit)
}

仔细观察可以发现,就是多了一个receiver参数,这个receiver参数可以为协程体提供一个作用域,在协程体内我们可以直接使用作用域内提供的函数或者状态等。

为了方便使用带有Receiver的的协程api,我们封装一个用来启动协程的函数launch:

复制代码
fun <R,T> launch(receiver:R,block:suspend R.()->T){
    block.startCoroutine(receiver,object:Continuation<T>{
        override val context: CoroutineContext
            get() = EmptyCoroutineContext

        override fun resumeWith(result: Result<T>) {
            println("Coroutine end:$result")
        }
    })
}

使用时先创建一个作用域,可以定义一个CoroutineScope类来创建一个协程的作用域,代码如下:

复制代码
class CoroutineScope{
    suspend fun produce(){
        println("生产一个产品")
    }
}

fun main() {
    launch(CoroutineScope()){
        produce()//直接使用作用域内提供的函数
        delay(1000)
    }
}

作用域可以用来提供函数支持,自然也可以用来增加限制。如果我们为Receiver对应的类型增加一个RestrictsSuspension注解,那么在它的限制下,在协程体内就不能调用外部的挂起函数了,也就是说如果调用delay函数就会出错。

(4)函数的挂起

我们已经知道使用suspend关键字可以声明一个挂起函数,挂起函数只能在协程体内或其他挂起函数中调用。这样一来,整个kotlin语言体系就可以分为两派:普通函数和挂起函数。其中挂起函数中可以调用任何函数,普通函数中只能调用普通函数。

但是,需要注意的是,挂起函数不一定真的会挂起,只是提供了挂起的条件。那什么时候才会挂起呢?在回答这个问题之前我们先来了解一个概念:挂起点,在协程内部挂起函数的调用处被称为挂起点,只有当挂起点处发生异步调用,当前协程才会被挂起,直到这个协程对应的continuation实例的resumeWith函数被调用时才会恢复执行。

(5)协程的上下文

协程的上下文用于提供协程启动和执行时所需要的信息,它是一个特殊的集合类型,有点像Map,集合中每个元素都是Element,并且有一个Key与之对应,Element之间可以通过"+"连接起来。主要有4种类型的Element:

  • Job:协程的唯一标识,用来管理协程的各个生命周期(new ,active,completing,completed,cancelling,cancelled)
  • CoroutineDispatcher:协程调度器,用于指定协程运行在哪个线程(IO,Main,Default,Unconfined)
  • CoroutineName:指定协程的名称
  • CoroutineExceptionHandler:指定协程的异常处理器,用来处理未捕获的异常

协程的标准库也为我们定义了一个空的协程上下文,EmptyCoroutineContext,里面没有任何数据。

(6)协程的拦截器

我们现在已经知道Kotlin协程可以通过调用挂起函数实现挂起,可以通过Continuation的恢复调用实现恢复,还知道协程可以通过绑定一个上下文来设置一些数据来丰富协程的能力,那么我们最关心的问题来了,协程如何处理线程的调度?答案就是通过拦截器,它可以拦截协程异步回调时的恢复调用,那么想要操纵线程的调度应该不是什么难事。

挂起点的恢复执行的位置可以添加拦截器来实现一些切片操作,定义拦截器只需要实现拦截器的接口,并添加到相应的协程上下文中即可。

复制代码
class LogInterceptor(override val key: CoroutineContext.Key<*>) :ContinuationInterceptor{
    override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
        println("恢复调用前")
        continuation.resumeWith(Result.success("恢复调用" as T))
        println("恢复调用后")
        return continuation
    }
}

这样一来,协程在挂起完执行恢复调用的前后就会打印出上面的日志,除了打印日志外,拦截器含有其他作用,调度器就是基于拦截器实现的,这块内容后面再提。