协程在Kotlin中文文档的解释是轻量级的线程,Go、Python 等很多现成语言在语言层面上都实现协程,不过Kotlin和他们不同的的是,Kotlin协程本质上只是一套基于原生Java线程池 的封装,Kotlin 协程的核心竞争力在于:它能简化异步并发任务,以同步方式写异步代码。
- 挂起:suspend
在协程里suspend是一个重要的关键字,这个关键字只是起到的提醒的作用,当代码执行到suspend时,会从当前线程挂起这个函数,然后代码继续执行,而挂起的函数从当前线程脱离,然后继续执行,这个时候在哪个线程执行,由协程调度器所指定,挂起函数执行完之后,又会重新切回到它原先的线程来。这个就是协程的优势所在。
private fun test3(){
Log.e("test", "start")
lifecycleScope.launch {
launch()
}
Log.e("test", "end")
}
suspend fun launch(){
Log.e("test", "launch_start")
delay(3000)
Log.e("test", "launch_end")
}
运行结果如下:
从截图可以看出,launch函数被挂起,然后主要流程继续执行,而launch函数被挂起后也继续执行。
- 挂起函数线程切换
从上面看我们已经挂起了函数,让程序脱离当前的线程,kotlin 协程提供了一个 withContext() 方法,来实现线程切换。在讲切线程之前,我们先说说Dispatchers调度器,它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行。
Dispatchers调度器种类
- Dispatchers.Main:Android 中的主线程
- Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
- Dispatchers.Default:适合 CPU 密集型的任务,比如计算
- Dispatchers.Unconfined:当我们不关心协程在哪个线程上被挂起时使用
那我们怎么切换线程呢,
suspend fun launch() {
withContext(Dispatchers.IO){
Log.e("test2", Thread.currentThread().name)
delay(3000)
Log.e("test", "launch_end")
}
}
在lifecycleScope里,就更简单了,直接如下
private fun test3() {
Log.e("test", "start")
lifecycleScope.launch(Dispatchers.IO) {
Log.e("test", "launch_start")
delay(3000)
withContext(Dispatchers.Main){
Log.e("test", "launch_end")
}
}
Log.e("test", "end")
}
在协程里,线程切换就是这么简单,在io线程执行耗时任务,然后又在main里切会主线程。
协程的取消,追踪协程的状态
我们开启了一个协程,在协程执行期间也想操作这个协程,这就是要用到Job,什么是Job,从概念上讲,一个 Job 表示具有生命周期的、可以取消的东西。从形态上将,Job 是一个接口,但是它有具体的合约和状态,所以它可以被当做一个抽象类来看待。
Job 一共包含六个状态:
- 新创建 New
- 活跃 Active
- 完成中 Completing
- 已完成 Completed
- 取消中 Cancelling
- 已取消 Cancelled
Job 的生命周期会经过四个状态:New → Active → Completing → Completed。
下面来讲讲job的常用方法:
- join() 挂起协程,直到任务完成再恢复
private suspend fun test() {
Log.e("test", "start")
job=lifecycleScope.launch(Dispatchers.IO) {
launch()
}
job?.join()
Log.e("test", "end")
}
private suspend fun launch() {
withContext(Dispatchers.IO){
Log.e("test2", "launch_start")
delay(3000)
Log.e("test", "launch_end")
}
}
运行结果如下:
可以看到,加上join,协程代码会在这里执行,并且是阻塞的,只有执行完才会走下一步
cancel() 取消协程
cancelAndJoin() 取消并挂起调用协程,直到被取消的协程完成
private suspend fun test() {
Log.e("test", "start")
job=lifecycleScope.launch(Dispatchers.IO) {
launch()
}
job?.cancelAndJoin()
Log.e("test", "end")
}
运行结果如下:
可以看待,cancelAndJoin(),会运行,然后取消,取消完后会走下一步
- start() 如果Job所在的协程还没有被启动那么调用这个方法就会启动协程,如果这个协程被启动了返回true,如果已经启动或者执行完毕了返回false
代码如下:
private suspend fun test3() {
Log.e("test", "start")
job=lifecycleScope.launch(start = CoroutineStart.LAZY) {
launch()
}
Log.e("test", "end")
job?.start()
}
运行效果如下:
可以看到当设置延迟加载时,协程是start()后才开始执行
说到延迟加载,在总结一下协程启动模式
DEFAULT 模式
默认的 协程启动模式 , 协程创建后 , 马上开始调度执行 , 如果在 执行前或执行时 取消协程 , 则进入 取消响应 状态 ; 如果在执行过程中取消 , 协程也会被取消 ;ATOMIC 模式
协程创建后 , 马上开始调度执行 , 协程执行到 第一个挂起点 之前 , 如果取消协程 , 则不进行响应取消操作 ;LAZY 模式
协程创建后 , 不会马上开始调度执行 , 只有 主动调用协程的 start , join , await 方法 时 , 才开始调度执行协程 , 如果在 调度之前取消协程 , 该协程直接报异常 进入异常响应状态 ;-
UNDISPATCHED 模式
协程创建后,立即在当前的函数调用栈执行协程任务,直到遇到第一个挂起函数,才在子线程中执行挂起函数 ;- 如果在主线程中启动协程 , 则该模式的协程就会直接在主线程中执行 ;
- 如果在子线程中启动协程 , 则该模式的协程就会直接在子线程中执行 ;
协程异常处理
对于不同协程构造器,异常的处理方式不同。分别介绍 launch 和 async 情况下的异常处理
- Launch
launch 方式启动的协程,异常会在发生时立刻抛出,使用 try catch 就可以将协程中的异常捕获。
scope.launch {
try {
codeThatCanThrowExceptions()
} catch(e: Exception) {
// Handle exception
}finally{
//结束处理
}
}
也可以try catch整个协程
try {
scope.launch {
codeThatCanThrowExceptions()
}
} catch(e: Exception) {
// Handle exception
}finally{
//结束处理
}
}
- Async
当 async 开启的协程为根协程 或 supervisorScope 的直接子协程时,异常在调用 await 时抛出,使用 try catch 可以捕获异常:
fun main() = runBlocking {
val deferred = GlobalScope.async {
throw Exception()
}
try {
deferred.await() //抛出异常
} catch (t: Throwable) {
println("捕获异常:$t")
}finally{
//结束处理
}
}
协程并行
到目前为止,上面的代码都是串行的,即从上到下依次执行,而协程不单单串行,我们也可以并行的方式。
- 使用 async 并发
private fun test() {
Log.e("test", "start")
lifecycleScope.async {
Log.e("test", "launch1_start")
delay(1000)
Log.e("test", "launch1_end")
}
lifecycleScope.async {
Log.e("test", "launch2_start")
delay(2000)
Log.e("test", "launch2_end")
}
Log.e("test", "end")
}
运行效果如下
可以看到两个协程可以并行执行,也可以用await()方法,代码如下:
private fun test4(){
lifecycleScope.launch {
val a=async {
delay(1000)
1
}
val b=async {
delay(5000)
2
}
var c=a.await()+b.await()
Log.e("test",c.toString())
}
}
通过await()方法,即使两个协程完成时间不一致,最终也可以一起运算。
协程-并发处理
从上面可以了解到,协程也是可以并发的,既然是并发,那同样也会出现像java多线程并发的问题,导致各种问题,协程本身也提供了两种方式处理并发:
- Mutex
Mutex 类似于 synchorinzed,协程竞争时将协程包装为 LockWaiter 使用双向链表存储。Mutex通俗点来说就是kotlin的锁,和java 的synchronized和RecentLock对应。
使用mutex.withLock {*} 即可实现数据的同步以简化使用,下面给个事例:
在没有加锁之前:
private fun test(){
repeat(5) {
GlobalScope.launch(Dispatchers.IO) {
delay(2000)
value++
Log.e("test",value.toString())
}
}
}
开启五个协程,同时运行,对value操作:
可以看到,顺序是乱的,而加了mutex之后呢:
var mutex= Mutex()
private fun test(){
repeat(5) {
GlobalScope.launch(Dispatchers.IO) {
delay(2000)
mutex.withLock {
value++
Log.e("test",value.toString())
}
}
}
}
运行结果如下:
可以看到,加锁之后,都会等上一个运行后之后在解锁,在运行下一个。
- Actors
一个 actor 是由协程、被限制并封装到该协程中的状态以及一个与其它协程通信的 通道 组合而成的一个实体。一个简单的actor 可以简单的写成一个函数,但是一个拥有复杂状态的actor更适合由类来表示。
有一个 actor 协程构建器,它可以方便地将 actor 的通道组合到其作用域中(用来接收消息)、组合发送 channel 与结果集对象,这样对actor的单个引用就可以作为其句柄持有。
使用 actor 的第一步是定义一个 actor 要处理的消息类。
// 计数器 Actor 的各种类型
sealed class CounterMsg
object IncCounter : CounterMsg() // 递增计数器的单向消息
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // 携带回复的请求
接下来定义一个函数,使用 actor 协程构建器来启动一个 actor:
// 这个函数启动一个新的计数器 actor
fun CoroutineScope.counterActor() = actor<CounterMsg> {
var counter = 0 // actor 状态
for (msg in channel) { // 即将到来消息的迭代器
when (msg) {
is IncCounter -> counter++
is GetCounter -> msg.response.complete(counter)
}
}
}
执行代码:
runBlocking {
val counterActor = counterActor() // 创建该 actor
repeat(100) {
launch {
repeat(1000) {
counterActor.send(IncCounter)
}
}
}
delay(3000)
// 发送一条消息以用来从一个 actor 中获取计数值
val response = CompletableDeferred<Int>()
counterActor.send(GetCounter(response))
println("Counter = ${response.await()}")
counterActor.close() // 关闭该actor
}
actor 可以修改自己的私有状态,但只能通过消息互相影响(避免任何锁定)。actor 在高负载下比锁更有效,因为在这种情况下它总是有工作要做,而且根本不需要切换到不同的上下文,这样效率更高。
协程的创建
写到这里,基本上把协程的基本用法都说了,最后要用协程,要知道这么创建协程吧,其实这里也有分的,所以才放在最后,假如是单单在kotlin里创建协程,就有三种方式
- 使用 runBlocking 顶层函数创建:
runBlocking {
...
}
通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。
- 使用 GlobalScope 单例对象创建:
GlobalScope.launch {
...
}
GlobalScope和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会只受整个应用程序的生命周期限制,且不能取消。
- 自行通过 CoroutineContext 创建一个 CoroutineScope 对象:
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
...
}
这是比较推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context 和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)。
Android平台协程创建
首先需要引用ktx库
implementation "androidx.lifecycle:lifecycle-runtime-ktx:版本号"
这个时候我们就可以在activity或者framgent直接使用lifecycleScope进行启动协程。就像我上面的代码实例一样。
lifecycleScope和lifecycle的生命周期一致,退出的时候也可以自动取消协程,不用自己手动取消。
同时,还扩展了lifecycleScope.launchWhenResumed , lifecycleScope.launchWhenCreated ,lifecycleScope.launchWhenStarted,
分别对应activity或者fragment的onResumed(),onCreated(),onStarted().
- viewLifecycleOwner
虽然fragment也可以用lifecycleScope,但是最好还是viewLifecycleOwner,因为Fragment与Fragment中的View的生命周期并不一致,需要让observer感知Fragment中的View的生命周期而非Fragment,
ViewModel中使用协程
同样引入扩展库
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:版本号"
引入库之后,我们就可以在ViewModel用viewModelScope来使用协程.
其他环境下使用协程
其他情况下的创建按照上面协程推荐的第三种方式即可