协程笔记

协程在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来使用协程.

其他环境下使用协程

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容