前言

协程是Kotlin中比较难的概念,也是其语言最重要的特性。协程这个概念并非是Kotlin首创,它在好久以前就被提出来了,但兴起却是近十年来,尤其是python、go等语言都支持协程,而且小弟认为Kotlin最被值得推崇作为Android开发的首选语言也是因为其支持协程。首次学习协程肯定会比较费力,就好像我们第一次接触线程的概念也是比较难懂的,所以我们先从理论学习,再慢慢深入到应用,然后多写多用。来吧,协程!

一、协程的基本概念

协程是什么

官方对于协程的定义是这样的:

协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

简单总结以下几点:

  • 协程是可以由程序自行控制挂起、恢复的程序(Kotlin协程中最关键的就是挂起函数,因为协程挂起的时候不会阻塞其他线程)
  • 协程可以用来实现多任务的协作执行
  • 协程可以用来解决异步任务控制流的灵活转移,解决掉繁杂的回调嵌套,让程序结构简化,可读性大大提高

协程的作用

  • 协程可以降低异步程序的设计复杂度,需要注意的是协程不能让代码异步,只能让异步代码更简单
  • 挂起和恢复可以控制执行流程的转移(比如网络请求时可以从UI线程切换到IO线程,执行完回调结果又可以切换回UI线程)
  • 异步逻辑可以用同步代码的形式写出
  • 同步代码比异步代码更灵活,更容易实现复杂业务

线程 VS 协程

线程(Thread)是指操作系统的线程,被称为内核线程,线程是抢占式运行,也就是一个线程时间到了,另外一个线程就会接上运行,另外线程切换或线程阻塞的开销都比较大;协程(Coroutine)则是指语言实现的协程,其主要讲究的是协作,运行在内核线程之上的,也就是协程依赖于线程,所以协程可以看做是一个非常轻量级的线程。协程的挂起是非阻塞的,开发者可以自由的控制协程的挂起和恢复,一个线程中可以创建任意数量的协程。

小结:协程可以将嵌套繁杂的异步编程简化成顺序表达,代码的可读性大大提高,并且协程了提供协程挂起方法,这是一种基本无代价且可控的操作去替代了阻塞线程的方法

二、协程的基本要素

挂起函数

什么是挂起函数?

suspend关键字修饰的函数就是挂起函数,而所谓的挂起就是切换线程后会被自动切回来的线程调度操作。

挂起函数作用域

挂起函数只能在其他挂起函数或者协程中调用。

挂起函数的挂起和恢复

  • 挂起函数调用时包含了协程挂起的语义
  • 挂起函数返回时则包含了协程恢复的语义

挂起函数的类型

挂起函数的类型和普通函数类型的区别只在于加上suspend关键字。例如:

suspend fun foo(){} 对应类型是: suspend () -> Unit

suspend fun foo(a:Int):String{ TODO() } 对应类型是: suspend (Int) -> String

挂起函数是如何做到挂起恢复的?

挂起函数的的挂起和恢复其实是通过Continuation这个接口来实现的,我们定义的

suspend fun foo(){}方法实际上的是这样的:

fun foo(continuation: Continuation<Unit>): Any{ TODO()}

但这个Continuation是编译器帮我们传的,所以在我们定义的函数中是隐藏的,所以挂起函数只能在其他挂起函数或者协程中调用,因为只有在这两情况里面才有这个Continuation

下面我们用Retrofit和GitHub开放的api是实现一下如何将异步回调写成挂起函数,这里主要是分为三步:

  • 使用suspendCoroutine获取挂起函数的Continuation
  • 请求成功回调的分支使用Continuation.resume(value)
  • 请求失败回调使用Continuation.resumeWithException(t)

首先对Retrofit初始化和写请求接口方法:

val githubServieApi by lazy {
    val retrofit = retrofit2.Retrofit.Builder()
        .client(OkHttpClient.Builder().addInterceptor {
            it.proceed(it.request()).apply {
                println("request: ${code()}")
            }
        }.build())
        .baseUrl("https://api.github.com")
        .addConverterFactory(GsonConverterFactory.create())
        .build()
    retrofit.create(GitHubApi::class.java)
}
interface GitHubApi {
    @GET("users/{login}")
    fun getUserCallback(@Path("login") login: String): Call<User>
}
data class User(val id: String, val name: String, val url: String)

改造异步回调成为挂起函数

suspend fun getUserSuspend(name: String) = suspendCoroutine<User> { continuation ->
    //enqueue的调用会把我们的操作切换到IO线程,所以真正的挂起必须是异步调用resume,包括切换到其他线程resume或者是单线程事件循环异步执行
    //如果直接调用resume不算挂起的
    githubServieApi.getUserCallback(name).enqueue(object: Callback<User> {
        override fun onFailure(call: Call<User>, t: Throwable) =
            continuation.resumeWithException(t)
        override fun onResponse(call: Call<User>, response: Response<User>) =
            response.takeIf { it.isSuccessful }?.body()?.let(continuation::resume)
                ?: continuation.resumeWithException(HttpException(response))
    })
}
suspend fun main(){
    val user = getUserSuspend("Jeremyzwc") //这里在idea会看到有一个箭头的模样,那这里就是一个挂起点
    println(user)
}
打印结果:
request: 200
User(id=20693153, name=Max_z, url=https://api.github.com/users/Jeremyzwc)

二、协程的创建

  • 协程是一段可执行的程序
  • 协程的创建通常需要一个函数 (suspend function)
  • 协程的创建也需要一个API (createCoroutine)

创建协程主要通过ContinuationcreateCoroutine方法,这里提供了带receiver和不带的两种方法:

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

这里要注意的是一个协程需要两个Continuation,其中一个是suspend函数本身执行需要一个Continuation实例在恢复时调用,即上面函数中completion,这个是传进来的,另外一个Continuation是返回值Continuation<Unit>,这是创建出来的协程的载体,receiver suspend函数会被传给该实例作为协程的实际执行体。

三、协程的启动

启动协程通过ContinuationstartCoroutine方法,这里一样提供了带receiver和不带的两种启动方式:

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

对于使用createCoroutine方法创建协程只要调用resume方法就可以启动协程:

suspend {
}.createCoroutine(object :Continuation<Unit>{
    override val context: CoroutineContext
        get() = TODO("Not yet implemented")
    override fun resumeWith(result: Result<Unit>) {
        TODO("Not yet implemented")
    }
}).resume(Unit)

如果使用startCoroutine方法则使用suspend函数直接调用就启动了协程,startCoroutine方法包含了协程的创建和启动:

suspend {
}.startCoroutine(object :Continuation<Unit>{
    override val context: CoroutineContext
        get() = TODO("Not yet implemented")
    override fun resumeWith(result: Result<Unit>) {
        // 协程执行完调用
        println("Coroutinue end with $result") 
    }
})

四、协程上下文

  • 协程执行过程中需要携带数据
  • 索引是CoroutineContextKey
  • 元素是CoroutineContextElement

CoroutineContext主要是作为数据的载体,对数据进行包装和存储。在协程中CoroutineContext会携带协程的名字、异常处理器等。

五、拦截器:ContinuationInterceptor

  • ContinuationInterceptor是协程上下文里面的一个元素:
interface ContinuationInterceptor : CoroutineContext.Element
  • 可以对协程上下文所在协程的Continuation进行拦截
interface ContinuationInterceptor : CoroutineContext.Element {
   fun <T> interceptContinuation(continuation: Continuation<T>):                   Continuation<T>
}

拦截Continuation最主要的就是切换线程,上面的interceptContinuation方法可以到传进去了一个Continuation<T>又返回了一个Continuation<T>,这个有点类似于Okhttp里面的拦截器中拦截request,返回response的操作。

总结一下Continuation的执行流程:

一般情况下我们会通过以下方式创建并启动一个协程:

suspend {
    ...
}.createCoroutine(...)

suspend {...}是协程的本体,也是协程逻辑执行的地方,它执行创建完之后会创建出一个SuspendLambda的类,这个类是Continuation的实现类,在标准库ContinuationImpl中。

如果suspend {...}中还有挂起函数的执行,那SuspendLambda会被SafeContinuation包装,也可以认为SafeContinuation是一个拦截器,对SuspendLambda进行拦截,这个SafeContinuation的作用主要是确保以下两点:

  • resume只被调用一次
  • 如果在当前线程调用栈上直接调用则不会挂起,所以真正的挂起一定要切线程的

类似下面这种情况:

suspend {
    //SafeContinuation仅在挂起点时出现
    a()
}.createCoroutine(...)
suspend fun a() = suspendCoroutine<Unit> { 
    thread { 
        //这里的resume实际调用的是SafeContinuation的resume
        it.resume(Unit)
    }
}

这里SafeContinuationresume执行完之后会恢复到suspend {...}的执行,所以最后也会调用到SuspendLambdaresume方法。

小结:

本篇文章主要介绍和学习Kotlin协程的基本概念和基本要素,了解Kotlin实现协程的流程。基本概念包括:

  • 协程的定义:挂起和恢复
  • 协程的作用:控制流程转移和异步逻辑同步化
  • 和线程的对比

基本要素主要是包括:

  • Continuation
  • 挂起函数(挂起函数类型和异步回调改写)
  • 协程的创建
  • 协程上下文(拦截器的作用)

下一篇文章我们将记录学习如何使用官方协程框架来实现协程。