协程知识总结

这篇是许久之前初学协程之时整理的笔记,今天偶然翻到便整理成md发出来。现在的我真的越来越难总结出这么多又臭又长的东西了。

协程

定义

官方描述:协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

协程与线程的区别

协程是编译器级别的,线程是系统级别的

优势

协程就像轻量级的线程,线程是由系统调度的,线程的阻塞和切换开销都很大。而协程依赖于线程,协程挂起和切换的时候不需要阻塞线程,几乎是无代价的,
一个线程里面可以创建任意多个协程

使用

runBlocking

private fun test1() {
    runBlocking {
        Log.e(TAG, "test1: 进入协程,准备延迟")
        delay(1000)
        Log.e(TAG, "test1: 延迟结束")

    }
    Log.e(TAG, "test1: 主线程")
}

runBlocking启动的协程任务会阻断当前线程,直到该协程执行结束。当协程执行结束之后,页面才会被显示出来。

GlobalScope.launch

调用方法如下:

private fun test2() {
    GlobalScope.launch{
        log("进入协程")
        delay(1000)
        log("协程执行完成")
    }
    log("主线程执行代码")
}

以上代码执行结果如下:
E/MainLog: test1: 主线程执行代码
E/MainLog: test1: 进入协程
E/MainLog: test1: 协程执行完成
结论:launch方法不会阻塞主线程

Async

简单使用

private fun test3() {
    GlobalScope.async {
        log("进入协程async")
        delay(1000)
        log("协程执行完成async")
    }
    log("执行主线程")
}

上面代码执行结果如下:
E/MainLog: test1: 执行主线程
E/MainLog: test1: 进入协程async
E/MainLog: test1: 协程执行完成async
结论:async不会阻塞主线程

launch方法中执行多个async

代码如下:

private fun test4() {
   GlobalScope.launch {
       val result1 = GlobalScope.async {
           delay(2000)
           "result1"
       }
       val result2 = GlobalScope.async {
           delay(1000)
           "result2"
       }

       log("两个async执行结果:${result1.await()} :: ${result2.await()}")
   }
}

上面代码中,在launch方法里面执行了两个async方法,得到两个结果,最终通过result.await()获取async执行的结果并打印。
:await() 方法只能在一个协程内部调用,在主线程调用会报错
代码中必须两个aysnc函数都返回了结果才会调用打印日志方法

suspend兰布达lambda实现

suspend的返回值在resumeWith中可以使用result.getOrNull()
获得

withContext

withContext不会创建新的协程,可以用来切换调度器的时候使用
代码示例:

 private fun test6() {
        GlobalScope.launch(Dispatchers.Main) {//协程在主线程开始
            val image = withContext(Dispatchers.IO) {  // 切换到 IO 线程,并在执行完成后切回 UI 线程
                log("withContext")
            }
//            doSomeThing
            log("toMainThread")
        }
    }

上面的代码,协程会在Main主线程创建,但是中间有一部分逻辑需要在子线程中执行,使用withContext实现。withContext中的逻辑执行完成后会再次切换回主线程。
withContext会阻塞协程

协程的挂起suspend

使用suspend修饰的函数在协程内部被调用的时候会让协程进入挂起状态,直到函数执行完成才会结束挂起状态。

协程的取消

fun main() = runBlocking {
    val job1 = launch { // ①
        log(1)
        delay(1000) // ②
        log(2)
    }
    delay(100)
    log(3)
    job1.cancel() // ③
    log(4)
}

上面代码的输出结果:1、3、4
因为delay是可以响应取消的,而job1被取消了所以不能输出2
如下代码,我们将job1中的delay加一个try/catch:那么输出会变为:
1、3、4、
cancelled. kotlinx.coroutines.JobCancellationException: Job was cancelled; job=StandaloneCoroutine{Cancelling}@e73f9ac、
2
也就是说我们调用job1.cancel的时候delay会抛出异常从而中断协程

fun main() = runBlocking {
    val job1 = launch { // ①
        log(1)
        try {
             delay(1000)
        }catch (e:Exception){
            log("cancelled. $e")// ②
        }
        log(2)
    }
    delay(100)
    log(3)
    job1.cancel() // ③
    log(4)
}

kotlin中的Thread使用方法

有两种方式创建线程,方法如下:

val thread = thread {

}
val thread1 = thread(start = false) {

}
thread1.start()

默认无参的方式就会自动启动,也可以手动启动(必须设置start=false)

GlobalScope的使用注意点

Global scope 通常用于启动*协程,这些协程在整个应用程序生命周期内运行,不会被过早地被取消。程序代码通常应该使用自定义的协程作用域。直接使用 GlobalScope 的 async 或者 launch 方法是强烈不建议的
GlobalScope 创建的协程没有父协程,GlobalScope 通常也不与任何生命周期组件绑定。除非手动管理,否则很难满足我们实际开发中的需求。所以,GlobalScope 能不用就尽量不用。
比如如果在我们的activity中使用协程如果用GlobalScope实现,那么退出activity后是无法停止协程的运行的。这个情景下我们应用用MainScope实现是更好的

协程相关知识

协程的创建、start、join、取消、完成

当一个协程创建后它就处于新建(New)状态,当调用Job的start/join方法后协程就处于活跃(Active)状态,这是运行状态,协程运行出错或者调用Job的cancel方法都会将当前协程置为取消中(Cancelling)状态, 处于取消中状态的协程会等所有子协程都完成后才进入取消 (Cancelled)状态,当协程执行完成后或者调用CompletableJob(CompletableJob是Job的一个子接口)的complete方法都会让当前协程进入完成中(Completing)状态, 处于完成中状态的协程会等所有子协程都完成后才进入完成(Completed)状态。

协程调度器

协程上下文(coroutine context)包含一个协程调度器(参阅 CoroutineDispatcher),协程调度器 用于确定执行协程的目标载体,即运行于哪个线程,包含一个还是多个线程。协程调度器可以将协程的执行操作限制在特定线程上,也可以将其分派到线程池中,或者让它无限制地运行
如下代码中的参数就是一种指定调度器的方式:
GlobalScope.async(context = Dispatchers.IO){

}
如果启动协程的方法中没有指定参数,那么协程会从他外部的协程作用域继承上下文和作用域,如下代码:
GlobalScope.async{

}

四种调度器介绍Default、IO、Unconfined、Main

参考:https://blog.csdn.net/c10WTiybQ1Ye3/article/details/114956973

Default、io

Dispatchers.Default和Dispatchers.IO内部都是线程池实现,它们的含义是把协程运行在共享的线程池中

Unconfined

Dispatchers.Unconfined的含义是不给协程指定运行的线程,在第一次被挂起(suspend)之前,由启动协程的线程执行它,但被挂起后, 会由恢复协程的线程继续执行,  如果一个协程会被挂起多次,  那么每次被恢复后,  都有可能被不同线程继续执行
示例:

main

Dispatchers.Main的含义是把协程运行在平台相关的只能操作UI对象的Main线程,所以它根据不同的平台有不同的实现

job的理解

参考:http://blog.chengyunfeng.com/?p=1087
CoroutineScope.launch 函数返回的是一个 Job 对象,代表一个异步的任务。Job 具有生命周期并且可以取消。 Job 还可以有层级关系,一个Job可以包含多个子Job,当父Job被取消后,所有的子Job也会被自动取消;当子Job被取消或者出现异常后父Job也会被取消。
除了通过 CoroutineScope.launch 来创建Job对象之外,还可以通过 Job() 工厂方法来创建该对象。默认情况下,子Job的失败将会导致父Job被取消,这种默认的行为可以通过 SupervisorJob 来修改。
下面的代码演示了子协程中抛出异常父协程会被取消执行的代码

SupervisorJob

暂无

协程启动参数

启动协程需要三样东西,分别是 上下文、启动模式、协程体,协程体 就好比 Thread.run 当中的代码

协程的上下文

子协程的默认上下文:
当一个协程在另外一个协程的协程作用域中启动时,它将通过 CoroutineScope.coroutineContext 继承其上下文,新启动的协程的 Job 将成为父协程的 Job 的子 Job。当父协程被取消时,它的所有子协程也会递归地被取消
但是,当使用 GlobalScope 启动协程时,协程的 Job 没有父级。因此,它不受其启动的作用域和独立运作范围的限制

协程的启动模式

这篇文章把启动模式说的很明白配图说的很好
DEFAULT 立即执行协程体
ATOMIC 立即执行协程体,但在开始运行之前无法取消
UNDISPATCHED 立即在当前线程执行协程体,直到第一个
suspend 调用
LAZY 只有在需要的情况下运行
使用协程模式的代码:
GlobalScope.launch(start = CoroutineStart.LAZY){
println(“haha”)
}

协程体

暂无

协程作用域

作用域的一些概念说明

在协程的源代码中有一个接口 CoroutineScope用来指定协程的作用域

CoroutineContext:协程的上下文
MainScope:实现了 CoroutineScope接口 同时是通过调度器调度到了主线程的协程作用域
GlobalScope:实现了CoroutineScope接口 同时执行了一个空的上下文对象的协程作用域
coroutineContext:这通过个方法可以在一个协程中启动协程是承袭他的上下文,同时内部的job将成为外部job 的子job,当一个父协程被取消的时候,所有它的子协程也会被递归的取消。
CoroutineScope(coroutineContext:CoroutineContext):通过传递一个协程上下文实现作用域的创建

假设我们的应用程序有一个具有生命周期的对象,但该对象不是协程。例如,我们正在编写一个Android应用程序,并在Android Activity中启动各种协程,以执行异步操作来获取和更新数据、指定动画等。当 Actovoty 销毁时,必须取消所有协程以避免内存泄漏。当然,我们可以手动操作上下文和 Job 来绑定 Activity 和协程的生命周期。但是,kotlinx.coroutines 提供了一个抽象封装:CoroutineScope。你应该已经对协程作用域很熟悉了,因为所有的协程构造器都被声明为它的扩展函数
我们通过创建与 Activity 生命周期相关联的协程作用域的实例来管理协程的生命周期。CoroutineScope 的实例可以通过 CoroutineScope() 或 MainScope() 的工厂函数来构建。前者创建通用作用域,后者创建 UI 应用程序的作用域并使用 Dispatchers.Main 作为默认的调度器
例,activity作用域的MainScope:

class Activity {
    private val mainScope = MainScope()
    
    fun destroy() {
        mainScope.cancel()
    }
    // to be continued ...}

作用域的代码示例CoroutineScope

1、activity中的协程指定协程作用域只作用于activity生命周期内

协程作用域MainScope

使用MainScope必须引入依赖:
implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8’

async和launch的异同

launch 与 async 这两个函数大同小异,都是用来在一个 CoroutineScope 内开启新的子协程的。不同点从函数名也能看出来,launch 更多是用来发起一个无需结果的耗时任务(如批量文件删除、创建),这个工作不需要返回结果。async 函数则是更进一步,用于异步执行耗时任务,并且需要返回值(如网络请求、数据库读写、文件读写),在执行完毕通过 await() 函数获取返回值

区别

1、async返回类型为Deferred, launch返回类型为job
2、async可以在协程体中自定义返回值,并且通过Deferred.await堵塞当前线程等待接收async协程返回的类型

其它协程相关的东西

withTimeout

下面这个代码,如果withTimeout内的代码执行时间超过了1300ms,那么会抛出异常

withTimeout(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}

withTimeoutOrNull

和withTimeout一样的效果,不过超时的时候不会抛出异常

val result = withTimeoutOrNull(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
    "Done" // 在它运行得到结果之前取消它
}
println("Result is $result")

suspendCoroutine

join方法妙用

需要调研的知识

suspend关键字(未开始)

Suspend 挂起函数

协程中更新ui方法

使用MainScope更新ui

使用withContext切换到主线程更新ui

协程用Dispatcher.Main创建协程可以更新ui

上一篇:常见的设计模式(单例模式)


下一篇:使用枚举方式写-单例模式,太棒了