前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >协程笔记

协程笔记

作者头像
HelloJack
发布2022-12-21 14:36:39
7770
发布2022-12-21 14:36:39
举报
文章被收录于专栏:Jack的Android之旅Jack的Android之旅

协程在Kotlin中文文档的解释是轻量级的线程,Go、Python 等很多现成语言在语言层面上都实现协程,不过Kotlin和他们不同的的是,Kotlin协程本质上只是一套基于原生Java线程池 的封装,Kotlin 协程的核心竞争力在于:它能简化异步并发任务,以同步方式写异步代码。

  • 挂起:suspend 在协程里suspend是一个重要的关键字,这个关键字只是起到的提醒的作用,当代码执行到suspend时,会从当前线程挂起这个函数,然后代码继续执行,而挂起的函数从当前线程脱离,然后继续执行,这个时候在哪个线程执行,由协程调度器所指定,挂起函数执行完之后,又会重新切回到它原先的线程来。这个就是协程的优势所在。
代码语言:javascript
复制
    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")
    }

运行结果如下:

image.png

从截图可以看出,launch函数被挂起,然后主要流程继续执行,而launch函数被挂起后也继续执行。

  • 挂起函数线程切换 从上面看我们已经挂起了函数,让程序脱离当前的线程,kotlin 协程提供了一个 withContext() 方法,来实现线程切换。在讲切线程之前,我们先说说Dispatchers调度器,它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行。
Dispatchers调度器种类
  1. Dispatchers.Main:Android 中的主线程
  2. Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
  3. Dispatchers.Default:适合 CPU 密集型的任务,比如计算
  4. Dispatchers.Unconfined:当我们不关心协程在哪个线程上被挂起时使用 那我们怎么切换线程呢,
代码语言:javascript
复制
suspend fun launch() {
    withContext(Dispatchers.IO){
        Log.e("test2", Thread.currentThread().name)
        delay(3000)
        Log.e("test", "launch_end")
    }
}

在lifecycleScope里,就更简单了,直接如下

代码语言:javascript
复制
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。

image.png

下面来讲讲job的常用方法:

  • join() 挂起协程,直到任务完成再恢复
代码语言:javascript
复制
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")
    }
}

运行结果如下:

image.png

可以看到,加上join,协程代码会在这里执行,并且是阻塞的,只有执行完才会走下一步

  • cancel() 取消协程
  • cancelAndJoin() 取消并挂起调用协程,直到被取消的协程完成
代码语言:javascript
复制
private suspend fun test() {
    Log.e("test", "start")
    job=lifecycleScope.launch(Dispatchers.IO) {
        launch()
    }
    job?.cancelAndJoin()
    Log.e("test", "end")
}

运行结果如下:

image.png

可以看待,cancelAndJoin(),会运行,然后取消,取消完后会走下一步

  • start() 如果Job所在的协程还没有被启动那么调用这个方法就会启动协程,如果这个协程被启动了返回true,如果已经启动或者执行完毕了返回false

代码如下:

代码语言:javascript
复制
private suspend fun test3() {
    Log.e("test", "start")
    job=lifecycleScope.launch(start = CoroutineStart.LAZY) {
        launch()
    }
    Log.e("test", "end")
    job?.start()
}

运行效果如下:

image.png

可以看到当设置延迟加载时,协程是start()后才开始执行

说到延迟加载,在总结一下协程启动模式

  • DEFAULT 模式 默认的 协程启动模式 , 协程创建后 , 马上开始调度执行 , 如果在 执行前或执行时 取消协程 , 则进入 取消响应 状态 ; 如果在执行过程中取消 , 协程也会被取消 ;
  • ATOMIC 模式 协程创建后 , 马上开始调度执行 , 协程执行到 第一个挂起点 之前 , 如果取消协程 , 则不进行响应取消操作 ;
  • LAZY 模式 协程创建后 , 不会马上开始调度执行 , 只有 主动调用协程的 start , join , await 方法 时 , 才开始调度执行协程 , 如果在 调度之前取消协程 , 该协程直接报异常 进入异常响应状态 ;
  • UNDISPATCHED 模式 协程创建后,立即在当前的函数调用栈执行协程任务,直到遇到第一个挂起函数,才在子线程中执行挂起函数 ;
    1. 如果在主线程中启动协程 , 则该模式的协程就会直接在主线程中执行 ;
    2. 如果在子线程中启动协程 , 则该模式的协程就会直接在子线程中执行 ;
协程异常处理

对于不同协程构造器,异常的处理方式不同。分别介绍 launch 和 async 情况下的异常处理

  • Launch launch 方式启动的协程,异常会在发生时立刻抛出,使用 try catch 就可以将协程中的异常捕获。
代码语言:javascript
复制
scope.launch {
    try {
        codeThatCanThrowExceptions()
    } catch(e: Exception) {
        // Handle exception
    }finally{
        //结束处理
    }
}

也可以try catch整个协程

代码语言:javascript
复制
    try {
        scope.launch {
            codeThatCanThrowExceptions()
        }
    } catch(e: Exception) {
        // Handle exception
    }finally{
        //结束处理
    }
}
  • Async 当 async 开启的协程为根协程 或 supervisorScope 的直接子协程时,异常在调用 await 时抛出,使用 try catch 可以捕获异常:
代码语言:javascript
复制
fun main() = runBlocking {
    val deferred = GlobalScope.async {
        throw Exception()
    }
    try {
        deferred.await() //抛出异常
    } catch (t: Throwable) {
        println("捕获异常:$t")
    }finally{
        //结束处理
    }
} 
协程并行

到目前为止,上面的代码都是串行的,即从上到下依次执行,而协程不单单串行,我们也可以并行的方式。

  • 使用 async 并发
代码语言:javascript
复制
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")
}

运行效果如下

image.png

可以看到两个协程可以并行执行,也可以用await()方法,代码如下:

代码语言:javascript
复制
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 {*} 即可实现数据的同步以简化使用,下面给个事例: 在没有加锁之前:

代码语言:javascript
复制
private fun test(){
    repeat(5) {
        GlobalScope.launch(Dispatchers.IO) {
            delay(2000)
            value++
            Log.e("test",value.toString())
        }
    }
}

开启五个协程,同时运行,对value操作:

image.png

可以看到,顺序是乱的,而加了mutex之后呢:

代码语言:javascript
复制
var mutex= Mutex()
private fun test(){
    repeat(5) {
        GlobalScope.launch(Dispatchers.IO) {
            delay(2000)
            mutex.withLock {
                value++
                Log.e("test",value.toString())
            }
        }
    }
}

运行结果如下:

image.png

可以看到,加锁之后,都会等上一个运行后之后在解锁,在运行下一个。

  • Actors

一个 actor 是由协程、被限制并封装到该协程中的状态以及一个与其它协程通信的 通道 组合而成的一个实体。一个简单的actor 可以简单的写成一个函数,但是一个拥有复杂状态的actor更适合由类来表示。

有一个 actor 协程构建器,它可以方便地将 actor 的通道组合到其作用域中(用来接收消息)、组合发送 channel 与结果集对象,这样对actor的单个引用就可以作为其句柄持有。

使用 actor 的第一步是定义一个 actor 要处理的消息类。

代码语言:javascript
复制
// 计数器 Actor 的各种类型
sealed class CounterMsg
object IncCounter : CounterMsg() // 递增计数器的单向消息
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // 携带回复的请求

接下来定义一个函数,使用 actor 协程构建器来启动一个 actor:

代码语言:javascript
复制
// 这个函数启动一个新的计数器 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)
        }
    }
}

执行代码:

代码语言:javascript
复制
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 顶层函数创建:
代码语言:javascript
复制
runBlocking {
    ...
}

通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。

  • 使用 GlobalScope 单例对象创建:
代码语言:javascript
复制
GlobalScope.launch {
    ...
}

GlobalScope和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会只受整个应用程序的生命周期限制,且不能取消。

  • 自行通过 CoroutineContext 创建一个 CoroutineScope 对象:
代码语言:javascript
复制
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    ...
}

这是比较推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context 和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)。

Android平台协程创建

首先需要引用ktx库

代码语言:javascript
复制
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中使用协程

同样引入扩展库

代码语言:javascript
复制
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:版本号"

引入库之后,我们就可以在ViewModel用viewModelScope来使用协程.

其他环境下使用协程

其他情况下的创建按照上面协程推荐的第三种方式即可

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-12-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Dispatchers调度器种类
  • 协程的取消,追踪协程的状态
  • 协程异常处理
  • 协程并行
  • 协程-并发处理
  • 协程的创建
  • Android平台协程创建
  • ViewModel中使用协程
  • 其他环境下使用协程
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档