Kotlin协程基础

1,769 阅读7分钟

进程、线程和协程

简单来说:线程是操作系统中最小的执行单元,进程是最小的资源管理单元。

线程属于进程,是程序的实际执行者,一个进程至少保函一个主线程,可包好多个子线程。无论是进程还是线程,都是由操作系统所管理的。

在一个典型的消费者/生产者模式中,若多个生产者向一个队列中写入数据,若干个消费者从队列中消费数据,如果队列满了,生产者阻塞,如果队列满了,消费者阻塞

这个过程中涉及到同步所、线程状态切换、线程上下文切换,每一个点都是损耗性能的操作。

协程

这就引出了协程的概念(不是买票的那个携程)。

协程时一个轻量级的线程,像一个进程可以有多个线程,一个线程可以有多个协程。

那么问题来了,为什么使用协程呢?它对比线程有什么优势?

看看大佬时是怎么定义协程的: 

协程的开发人员 Roman Elizarov 是这样描述协程的:协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。

Kotlin中的协程

官方文档中的介绍:

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

协程在线程中是顺序执行的,那怎么实现异步操作呢?

Thread 中有阻塞、唤醒的概念,协程里同样也有类似概念,挂起等同于阻塞,当线程接收到某个协程的挂起请求后,会去执行其他计算任务,比如其他协程。协程这样来实现多线程、异步的效果,和Thread对比会有一些区别

依赖

在module的build.gradle中添加如下依赖,目前在maven仓库中找打的最新版本是v1.5.2

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'

创建协程

kotlin中没有new,因此协程创建不是使用类似的new方法进行,而是使用kotlin协程封装的函数:

  • runBlocking:独立使用,代码块中的delay操作会阻塞线程,另外3个都是GlobalScope的API
  • launch:创建线程
  • async:创建一个带返回值的协程,返回值类型为Job的子类Deferred
  • withContext:不创建新的协程,而是在指定的协程内执行代码块

例:

println("协程未开始:${Date()} ")
runBlocking {
    println("协程开始:${Date()} ")
    for (i in 1..5) {
        println("协程执行[$i]:${Date()} ")
        delay(500L)
    }
    println("协程结束:${Date()} ")
}
println("协程已结束:${Date()} ")

执行结果

I/System.out: 协程未开始:Fri Sep 17 05:04:36 GMT+08:00 2021 
I/System.out: 协程开始:Fri Sep 17 05:04:36 GMT+08:00 2021 
    协程执行[1]:Fri Sep 17 05:04:36 GMT+08:00 2021 
I/System.out: 协程执行[2]:Fri Sep 17 05:04:37 GMT+08:00 2021 
I/System.out: 协程执行[3]:Fri Sep 17 05:04:37 GMT+08:00 2021 
I/System.out: 协程执行[4]:Fri Sep 17 05:04:38 GMT+08:00 2021 
I/System.out: 协程执行[5]:Fri Sep 17 05:04:38 GMT+08:00 2021 
I/System.out: 协程结束:Fri Sep 17 05:04:39 GMT+08:00 2021 
I/System.out: 协程已结束:Fri Sep 17 05:04:39 GMT+08:00 2021 

launch函数

函数定义

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

参数:

  • context:协程的上下文,分为四种Dispatchers.Default、Dispatchers.IO、Dispatchers.Main、Dispatchers.Unconfined(不指定,使用当前线程)
  • start:启动模式,DEFAULT、ATOMIC、UNDISPATCHED、LAZY(懒加载,被调用时才执行)
  • block:闭包方法体,协程执行的内容

返回值:Job,协程的可操作方法,主要包括

  • job.start(),启动协程,懒加载时手动启动
  • job.join():同Thread.join,等待另一个协程执行完毕
  • job.cancel():取消一个协程
  • job.cancelAndJoin():等待协程执行完毕,然后取消

例:

GlobalScope.launch(Dispatchers.IO) {
    println("协程开始:${Date()} ")
    for (i in 1..3) {
        println("协程执行任务1打印[$i]:${Date()} ")
    }
    delay(500)
    for (i in 1..3) {
        println("协程执行任务2打印[$i]:${Date()} ")
    }
}

//懒加载
var job = GlobalScope.launch( start = CoroutineStart.LAZY ){
    println("协程开始:${Date()} ")
}

delay(500)
job.start()//启动后,协程开始执行

async函数

与launch相同,区别是async是有返回值的

GlobalScope.launch(Dispatchers.Unconfined) {
  val deferred = GlobalScope.async {
      delay(5000L)
      println("async执行")
      return @async "king"
  }

  println("协程开始:${Date()}")
  val result = deferred.await()
  println("协程返回值:$result")
  println("协程结束:${Date()}")
}

async返回值类型是Deferred,继承自Job,并扩展了一个await方法,用于接收block闭包中的返回值,async不会阻塞当前线程,但是会挂起协程(阻塞协程)

取消

aunch{}返回Job,async{}返回Deffer,Job和Deffer都有cancel()方法,用于取消协程。

异常

Kotlin协程的异常有两种:

  • 因协程取消,协程内部suspend方法抛出的CancellationException
  • 常规异常,这类异常,有两种异常传播机制
    • launch:将异常自动向父协程抛出,将会导致父协程退出
    • async: 将异常暴露给用户(通过捕获deffer.await()抛出的异常)
//官方文档的例子
fun main() = runBlocking {
    val job = GlobalScope.launch { // root coroutine with launch
        println("Throwing exception from launch")
        throw IndexOutOfBoundsException()
    }
    job.join()
    println("Joined failed job")
    val deferred = GlobalScope.async { // root coroutine with async
        println("Throwing exception from async")
        throw ArithmeticException() // 没有打印任何东西,依赖用户去调用等待
    }
    try {
        deferred.await()
        println("Unreached")
    } catch (e: ArithmeticException) {
        println("Caught ArithmeticException")
    }
}

输出,可查看异常的传递

Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
Joined failed job
Throwing exception from async
Caught ArithmeticException

全局处理异常

//全局handler
val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception") 
}

val job = GlobalScope.launch(handler) { // 根协程,运行在 GlobalScope 中
    throw AssertionError()
}
val deferred = GlobalScope.async(handler) { // 同样是根协程,但使用 async 代替了 launch
    throw ArithmeticException() // 没有打印任何东西,依赖用户去调用 deferred.await()
}
joinAll(job, deferred)

协程的挂起与恢复

协程中的函数默认是顺序调用的

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // 耗时操作
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // 耗时操作
    return 29
}

val time = measureTimeMillis {
    val one = doSomethingUsefulOne()
    val two = doSomethingUsefulTwo()
    println("The answer is ${one + two}")
}
println("Completed in $time ms")

输出结果

The answer is 42
Completed in 2017 ms

suspend

suspend 表示挂起的意思,用来修饰方法的,一个协程内有多个 suspend 修饰的方法顺序书写时,代码也是顺序运行的

协程的上下文

上面讲了,协程启动时可以指定上下文,

协程的上下文,分为四种Dispatchers.Default、Dispatchers.IO、Dispatchers.Main、Dispatchers.Unconfined

Dispatchers.Default

默认调度器。它使用JVM的共享线程池,该调度器的最大并发度是CPU的核心数,默认为2

Dispatchers.IO

IO调度器,他将阻塞的IO任务分流到一个共享的线程池中,使得不阻塞当前线程。该线程池大小为环境变量kotlinx.coroutines.io.parallelism的值,默认是64或核心数的较大者。

该调度器和Dispatchers.Default共享线程,因此使用withContext(Dispatchers.IO)创建新的协程不一定会导致线程的切换。

Dispatchers.Main

该调度器限制所有执行都在UI主线程,它是专门用于UI的,Android中表示是UI主线程

Dispatchers.Unconfined

非受限调度器,它不会将操作限制在任何线程上执行——在发起协程的线程上执行第一个挂起点之前的操作,在挂起点恢复后由对应的挂起函数决定接下来在哪个线程上执行。

简单来说就是:从哪挂起的从哪恢复

使用async并发

两个协程方法直接没有依赖关系,可以使用并发更快获取结果

val time = measureTimeMillis {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

使用async的结构化并发

suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

这种情况下,如果在 concurrentSum 函数内部发生了错误,并且它抛出了一个异常, 所有在作用域中启动的协程都会被取消。

作用域

协程作用域——CoroutineScope,用于管理协程,管理的内容有

  • 启动协程的方式 - 它定义了launch、async、withContext等协程启动方法(以extention的方式),并在这些方法内定义了启动子协程时上下文的继承方式。
  • 管理协程生命周期 - 它定义了cancel()方法,用于取消当前作用域,同时取消作用域内所有协程。

区分作用域和上下文

从定义看,CoroutineScope和CoroutineContext类似,最终目的都是协程上下文,但Kotlin协程负责人Roman Elizarov在Coroutine Context and Scope中表示,二者的区别只在于使用目的的不同——作用域用于管理协程;而上下文只是一个记录协程运行环境的集合。他们的关系如下

异步流

Kotlin中的异步流和RxJava中的流在概念上非常类似,可以被归为响应式流。并且Kotlin也提供响应的库将它转换为其它响应式流

  • kotlinx-coroutines-reactive 用于Reactive Streams
  • kotlinx-coroutines-reactor 用于Project Reactor
  • kotlinx-coroutines-rx2 用于RxJava2

使用异步流

  val flow = flow {
    // 耗时操作1
    delay(1000L)
    emit(12)
    // 耗时操作2
    delay(1000L)
    emit(13)
  }

  runBlocking {
    flow.collect { println(it) }
  }

通道

直接使用Channel构造

val channel = Channel<Int>()
launch {
    for (x in 1..5) channel.send(x * x)
    channel.close()
}
for (y in channel) println(y)
println("Done!")

使用produce

val channel = produce {
    send(12)
    send(13)
}

for (value in channel) {
    println(value)
}

\