美文网首页
协程笔记

协程笔记

作者: Jack921 | 来源:发表于2022-12-20 17:21 被阅读0次

    协程在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")
        }
    
    

    运行结果如下:


    image.png

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

    • 挂起函数线程切换
      从上面看我们已经挂起了函数,让程序脱离当前的线程,kotlin 协程提供了一个 withContext() 方法,来实现线程切换。在讲切线程之前,我们先说说Dispatchers调度器,它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行。
    Dispatchers调度器种类
    1. Dispatchers.Main:Android 中的主线程
    2. Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
    3. Dispatchers.Default:适合 CPU 密集型的任务,比如计算
    4. 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。

    image.png

    下面来讲讲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")
        }
    }
    
    

    运行结果如下:


    image.png

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

    • cancel() 取消协程

    • cancelAndJoin() 取消并挂起调用协程,直到被取消的协程完成

    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

    代码如下:

    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 就可以将协程中的异常捕获。
    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")
    }
    

    运行效果如下

    image.png

    可以看到两个协程可以并行执行,也可以用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操作:


    image.png

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

    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 要处理的消息类。

    // 计数器 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来使用协程.

    其他环境下使用协程

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

    相关文章

      网友评论

          本文标题:协程笔记

          本文链接:https://www.haomeiwen.com/subject/qrzfqdtx.html