进程、线程和协程
简单来说:线程是操作系统中最小的执行单元,进程是最小的资源管理单元。
线程属于进程,是程序的实际执行者,一个进程至少保函一个主线程,可包好多个子线程。无论是进程还是线程,都是由操作系统所管理的。
在一个典型的消费者/生产者模式中,若多个生产者向一个队列中写入数据,若干个消费者从队列中消费数据,如果队列满了,生产者阻塞,如果队列满了,消费者阻塞
这个过程中涉及到同步所、线程状态切换、线程上下文切换,每一个点都是损耗性能的操作。
协程
这就引出了协程的概念(不是买票的那个携程)。
协程时一个轻量级的线程,像一个进程可以有多个线程,一个线程可以有多个协程。
那么问题来了,为什么使用协程呢?它对比线程有什么优势?
看看大佬时是怎么定义协程的:
协程的开发人员 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)
}
\