Kotlin协程N问(一)基础知识

871 阅读4分钟

1、什么是Job 、Deferred 、协程作用域

  • Job:我们可以认为他就是一个协程作业是通过CoroutineScope.launch生成的
  • Deferred继承自Job,我们可以把它看做一个带有返回值的Job
  • 协程作用域(Coroutine Scope)是协程运行的作用范围。launchasync都是CoroutineScope的扩展函数 CoroutineScope定义了新启动的协程作用范围,

2、作用域 runBlocking launch async

  • 同一个作用域里的

  • runBlocking:T 启动一个新的协程并阻塞调用它的线程,直到里面的代码执行完毕,返回值是泛型T,就是你协程体中最后一行是什么类型,最终返回的是什么类型T就是什么类型。

  • launch:Job 启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用,返回值是一个Job。

  • async:Deferred<T> 启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用。以Deferred对象的形式返回协程任务。返回值泛型TrunBlocking类似都是协程体最后一行的类型。

val runBlockingJob = runBlocking { 
    Log.d("Coroutine", "runBlocking启动一个协程") "我是runBlockingJob协程的返回值"
}


onCreate(){
log2("Main----")
CoroutineScope(Dispatchers.Main).launch{
    log2("Main1")
    val reuslt = fetchDocs()
    log2("Main2")
}
log2("Main3")
}


suspend fun fetchDocs() {
    log2("fetchDocs")   // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    log2(result)                                  // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) {
    /* ... */
    delay(1000*10)
    log2("withContext")
    "kentwang" }
fun log2(msg:Any?) = Log.d("ktag","${Thread.currentThread().name} $msg")

3、协程的并发与同步

  • Dispatchers.Main
    • 子协程会按照顺序执行
  • 非Dispatchers.Main
    • 子协程无序执行

4、协程调度器CoroutineDispatcher

  • Default:默认调度器,CPU密集型任务调度器,适合处理后台计算。通常处理一些单纯的计算任务,或者执行时间较短任务。比如:Json的解析,数据计算等
  • IO:IO调度器,,IO密集型任务调度器,适合执行IO相关操作。比如:网络处理,数据库操作,文件操作等
  • Main:UI调度器, 即在主线程上执行,通常用于UI交互,刷新等
  • Unconfined:非受限调度器,又或者称为“无所谓”调度器,不要求协程执行在特定线程上。

5、协程上下文

  • 三个上下文中的Job是同一个对象。
  • 第二个上下文在第一个的基础上增加了一个新的CoroutineName,新增的CoroutineName替换了第一个上下文中的CoroutineName
  • 第三个上下文在第二个的基础上又增加了一个新的CoroutineNameDispatchers,同时他们也替换了第二个上下文中的CoroutineNameDispatchers。协程不会被绑定到特定的线程或线程池,而是在调用协程的线程上直接执行。
var job = GlobalScope.launch {  }
var coroutineContext1 = job+CoroutineName("这是上下文的名字")
Log.d("coroutineContext1", "$coroutineContext1")
coroutineContext1 = coroutineContext1+ Dispatchers.Default + CoroutineName("上下文这是为Default的")
Log.d("coroutineContext1", "$coroutineContext1")
coroutineContext1 = coroutineContext1 + Dispatchers.Main + CoroutineName("第三个上下文  设置成了Main")
Log.d("coroutineContext1", "$coroutineContext1")
 D/coroutineContext1: [StandaloneCoroutine{Active}@bee37ba, CoroutineName(这是上下文的名字)]
 D/coroutineContext1: [StandaloneCoroutine{Active}@bee37ba, CoroutineName(上下文这是为Default的), Dispatchers.Default]
 D/coroutineContext1: [StandaloneCoroutine{Active}@bee37ba, CoroutineName(第三个上下文  设置成了Main), Dispatchers.Main]

6、启动模式

6.2.1、DEFAULT

  • 默认启动模式,创建完之后立即调度,并非立即执行,可能再执行前被取消

6.2.2、LAZY

  • 只有我们需要它执行是才调度,也就是Job主动调用start join await

6.3.3、ATOMIC

  • 原子模式,创建之后就调度,再挂起点到来之前,即使调用了cancel也不会理你

6.3.4、UNDISPATCHED

  • 不调度模式,创建之后立马执行,不需要调度器。等到挂起挂起之后恢复才需要调度器

7、协程作用域

  • 顶级作用域 --> 没有父协程的协程所在的作用域称之为顶级作用域。

  • 协同作用域 --> 在协程中启动一个协程,新协程为所在协程的子协程。子协程所在的作用域默认为协同作用域。此时子协程抛出未捕获的异常时,会将异常传递给父协程处理,如果父协程被取消,则所有子协程同时也会被取消。

  • 主从作用域 官方称之为监督作用域。与协同作用域一致,区别在于该作用域下的协程取消操作的单向传播性,子协程的异常不会导致其它子协程取消。但是如果父协程被取消,则所有子协程同时也会被取消。

7.1、supervisorScope和SupervisorJob

  • 使用可以避免协同作用域下的其它协程因为其中一个协程抛出异常导致其它协程停止运行。

  • GlobalScope 中启动的子协程是彼此独立的,一个顶级子协程的异常会导致其他顶级子协程被取消。 相比之下,在 supervisorScope 中启动的子协程具有容错的特性,一个顶级子协程的异常不会影响其他顶级子协程的执行

CoroutineScope + SupervisorJob 和 supervisorScope 提供了类似的容错能力,但它们在使用方式和范围上有所不同。

  1. 范围不同CoroutineScope + SupervisorJob 可以在任何协程作用域中使用,例如在顶级函数中、在其他协程作用域中或在类中。它可以用于创建具有容错能力的子协程集合,并且该容错特性仅适用于创建该作用域的子协程。

    supervisorScope 是一个挂起函数,只能在挂起函数或其他 suspend 函数中使用。当在 supervisorScope 中启动的子协程发生异常时,只会取消当前 supervisorScope 内的子协程,而不会影响其他作用域或父协程中的子协程。

  2. 取消行为不同: 使用 CoroutineScope + SupervisorJob,可以通过取消关联的 SupervisorJob 来取消整个作用域及其包含的子协程。取消 SupervisorJob 会同时取消该作用域中的所有子协程。

    使用 supervisorScope,取消 supervisorScope 的效果是取消当前 supervisorScope 内的子协程,但不会取消父协程或其他 supervisorScope

//用 supervisorScope 即使内部抛出异常但是不会影响同一GlobalScope内其它子协程
fun test07() {
    val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable_ ->
        Log.d(TAG, "test07: ${coroutineContext[CoroutineName]} $throwable_");
    }

    GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1") + coroutineExceptionHandler) {
        supervisorScope {
            Log.d("scope", "--------- 1")
            launch(CoroutineName("scope2")) {
                Log.d("scope", "--------- 2")
                throw  NullPointerException("空指针")
                Log.d("scope", "--------- 3")
                val scope3 = launch(CoroutineName("scope3")) {
                    Log.d("scope", "--------- 4")
                    delay(2000)
                    Log.d("scope", "--------- 5")
                }
                scope3.join()
            }


            val scope4 = launch(CoroutineName("scope4")) {
                Log.d("scope", "--------- 6")
                delay(2000)
                Log.d("scope", "--------- 7")
            }
            scope4.join()
            Log.d("scope", "--------- 8")

        }
    }

    /**
     * D/scope: 1--------- CoroutineName(scope2)
    D/exceptionHandler: CoroutineName(scope2) java.lang.NullPointerException: 空指针
    D/scope: 2--------- CoroutineName(scope3)
    D/scope: 4--------- CoroutineName(coroutineScope)
    D/scope: 5--------- CoroutineName(coroutineScope)
    D/scope: 6--------- CoroutineName(scope1)
     */

    fun test08() {
        val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
            Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable")
        }

        val coroutineScope = CoroutineScope(SupervisorJob() + CoroutineName("coroutineScope"))
        GlobalScope.launch(Dispatchers.Main+CoroutineName("scope1")+exceptionHandler){
            with(coroutineScope){
                val scope2 = launch(CoroutineName("scope2") + exceptionHandler) {
                    Log.d("scope", "1--------- ${coroutineContext[CoroutineName]}")
                    throw  NullPointerException("空指针")
                }
                val scope3 = launch(CoroutineName("scope3") + exceptionHandler) {
                    scope2.join()
                    Log.d("scope", "2--------- ${coroutineContext[CoroutineName]}")
                    delay(2000)
                    Log.d("scope", "3--------- ${coroutineContext[CoroutineName]}")
                }
                scope2.join()
                Log.d("scope", "4--------- ${coroutineContext[CoroutineName]}")
                coroutineScope.cancel()
                scope3.join()
                Log.d("scope", "5--------- ${coroutineContext[CoroutineName]}")
            }
            Log.d("scope", "6--------- ${coroutineContext[CoroutineName]}")
        }
    }
}

8、挂起函数

  • Kotlin中用suspend关键字修饰,允许函数在运行时暂时挂起,并将控制权交还给调用这。特点就是不阻塞线程的情况下执行长时间运行的任务,而且保持程序响应。
  • 挂起函数只能在协程体内或者其它挂起函数内调用
  • 挂起函数可以调用任何函数,普通函数却不能调用挂起函数
  • 当协程在挂起点处遇到异步调用时,当前协程会被挂起,直到对应的Continuation.resume()函数被调用才会恢复执行
fun test10() = runBlocking {
    val result = async {
        suspendCancellableCoroutine<Int> { cont ->
            Log.d("test10","协程挂起")
            GlobalScope.launch {
                Log.d("test10","异步操作完成,恢复协程")
                // 恢复协程并传递结果
                cont.resume(5)
            }
        }
    }
    // 等待异步操作完成并获取结果
    Log.d("test10","挂起函数的结果:${result.await()}")
}

9、协程异常处理

9.1 协程构建器

  • launch和actor。在协程内部就要trycatch,不然会传播出去就崩了
  • async 和produce,则是在调用时捕获,比如在Deferred调用await时捕获

9.2 协程异常的产生流程

- 通过CoroutineScheduler创建一个Thread线程(Worker),然后执行(runWorker),
- 线程里通过executeTask执行一个任务DispatchedTask。
- 任务执行时try catch  保证任务runSafely。
- 运行异常时通过catch里的resumeWith来告知结果线程出问题了

9.3 协程的异常处理

  • 一种是通过resumeWithException抛出。在协程内部就要trycatch
fun main() = runBlocking {  
    launch {  
        delay(1000) // 模拟耗时操作  
        try {  
            if (isActive) {  
                throw Exception("模拟异常") // 抛出异常  
            }  
            println("任务完成")  
        } catch (e: Exception) {  
            // 在协程内部捕获异常并处理  
            println("捕获到异常: $e")  
        }  
    }  
}
  • 一种是通过CoroutineExceptionHandler抛出。
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    Log.d("exceptionHandler", "${coroutineContext[CoroutineName]}$throwable")
}

GlobalScope.launch(CoroutineName("异常处理")+coroutineExceptionHandler) {
    val job = launch{
        Log.d("${Thread.currentThread().name}","我要开始抛异常了" )
        throw NullPointerException("异常测试")
    }
    Log.d("${Thread.currentThread().name}", "end")
}

10、小总结

  • 协程的基础使用方式和基本原理。

  • CoroutineContext:协程上下文中包含的Element以及下上文的作用,传递。CoroutineParent和CoroutineExceptionHandler

  • CoroutineDispatcher:协程调度器的使用。就是Default(CPU) IO Main

  • CoroutineStart:协程启动模式在不同模式下的区别。DEFAULT LAZY AOTMIC UNDISPATCHED

  • CoroutineScope:协程作用域的分类,以及不同作用域下的异常处理。顶级 协同 主从

  • 挂起函数以及suspend关键字的作用,以及Continuation的挂起恢复流程。

  • CoroutineExceptionHandler:协程异常处理,结合supervisorScopeSupervisorJob的使用。