美文网首页
Kotlin 协程基本概念

Kotlin 协程基本概念

作者: 三天过去了 | 来源:发表于2022-09-05 16:45 被阅读0次

    目录

    • 1、什么是 Kotlin 协程
    • 2、场景举例
    • 3、如何使用 Kotlin 协程
    • 4、实现第一个协程
    • 5、CoroutineScope(作用域)
      • 5.1、GlobalScope
      • 5.2、runBlocking
      • 5.3、coroutineScope
      • 5.4、supervisorScope
    • 6、suspend(挂起)
    • 7、CoroutineContext(协程上下文)
      • 7.1、Dispatchers(协程调度器)
      • 7.2、CoroutineName(协程命名)
      • 7.3、CoroutineExceptionHandler(协程异常捕捉)
      • 7.4、组合上下文
    • 8、CoroutineBuilder(协程构建器)
      • 8.1、launch
      • 8.2、async
    • 9、参考

    1、什么是 Kotlin 协程

    本质上是一个轻量级的线程,可以很方便实现线程间切换,并且支持非阻塞式的挂起。

    2、场景举例

    我们需要同时请求多个接口,并且把返回值组装起来,按照 callback 方式伪代码如下。

    HttpRequest(context).url("/xxx/xxx").callback(object : HttpCallback<String>() {
    
        override fun onResponse(response: String?) {
            // 第一个接口请求成功,发起第二个请求
            HttpRequest(context).url("/xxx/xxx").callback(object : HttpCallback<String>() {
    
                override fun onResponse(response: String?) {
                    // 第二个接口请求成功
                    // do something
                }
            }).get()
        }
    }).get()
    

    使用 callback 的方式,使得本可以同时请求的接口,不得不变成的顺序执行,从流程上接口调用时间增加了一倍,且可读性很差。

    3、如何使用 Kotlin 协程

    由于协程不在 Kotlin 基础库中,所以需要添加依赖:

    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
    

    4、实现第一个协程

    fun main() {
        log("start")
        GlobalScope.launch(context = Dispatchers.IO) {
            // 延时 1000 ms
            delay(1000)
            // log 打印当前线程的名称
            log("launch")
        }
        // 休眠 2000 ms    
        Thread.sleep(2000)
        log("end")
    }
    
    xxx 16:13:29.045 2006-2006/xxx D/qqq: [main] start
    xxx 16:13:30.098 2006-3050/xxx D/qqq: [DefaultDispatcher-worker-1] launch
    xxx 16:13:31.092 2006-2006/xxx D/qqq: [main] end
    

    例子中,通过 GlobalScope 启动了一个协程,在延迟一秒后输出一行日志。从输出结果可以看出,启动的协程是运行在协程内部的线程池中。虽然从表现结果来看,启动一个协程类似于我们直接使用 Thread 来执行耗时任务,但实际上协程和线程有着本质上的区别。通过使用协程,可以极大的提高线程的并发效率,避免以往嵌套的回调地狱,极大的提高了代码的可读性。

    以上代码涉及了协程的四个基本概念:

    • CoroutineScope,即协程作用域,GlobalScope 是 CoutineScope 的一个实现类,用于指定协程的作用范围,可用于管理多个协程的生命周期。
    • suspend function,即挂起函数,这里的 delay() 就是协程库提供的一个非阻塞式延时的挂起函数。
    • CoroutineContext,即协程上下文,Dispatcher.IO 就是其中一种配置参数,用于指定协程运行在哪一类线程上。
    • CoroutineBuilder,即线程构建器,通过 launch、async 等协程构建器来进行声明并启动。launch、async 均为 CoroutineScope 的扩展函数。

    5、CoroutineScope(作用域)

    CoroutineScope 即协程作用域,用于指定协程的作用范围,并可以进行统一管理。所有的协程都需要通过 CoroutineScope 来启动,它会跟踪创建的所有协程,可以调用 scope.cancel() 取消正在运行的协程。在 Android 中,某些 ktx 库为某些生命周期类提供了自己的 CoroutineScope,例如 ViewModel 的 viewModelScope,Lifecycle 的 lifecycleScope

    CoroutineScope 大体上可以分为三种:

    • GlobalScope,即全局协程作用域,内部协程可以一直运行到应用停止运行,不会阻塞当前线程,且启动的协程相当于守护线程,不会阻止 JVM 结束运行。
    • runBlocking,一个顶层函数,和 GlobalScope 不一样,它会阻塞当前线程,直到其内部所有相同作用域的协程执行结束。
    • 自定义 CoroutineScope,可用于实现主动控制协程的生命周期范围,对于 Android 开发来说最大意义之一就是可以在 Activity、Fragment、ViewModel 等具有生命周期的对象中按需取消所有协程任务,从而确保生命周期安全,避免内存泄漏。

    5.1、GlobalScope

    GlobalScope 是全局作用域,通过它启动的协程的生命周期,只会受整个应用程序的生命周期限制。只要应用程序还在运行,且协程任务还没有结束,就可以一直运行。

    fun startGlobalScope() {
        log("start")
        // GlobalScope 是 CoroutineScope 的实现类
        GlobalScope.launch {
            launch {
                // delay 是非阻塞的,有 suspend 修饰符
                delay(400)
                log("launch A")
            }
            launch {
                delay(300)
                log("launch B")
            }
            log("GlobalScope")
        }
        log("end")
        Thread.sleep(500)
    }
    
    xxx 17:31:08.023 7142-7142/xxx D/qqq: [main] start
    xxx 17:31:08.058 7142-7142/xxx D/qqq: [main] end
    xxx 17:31:08.062 7142-7324/xxx D/qqq: [DefaultDispatcher-worker-1] GlobalScope
    xxx 17:31:08.366 7142-7326/xxx D/qqq: [DefaultDispatcher-worker-2] launch B
    xxx 17:31:08.466 7142-7326/xxx D/qqq: [DefaultDispatcher-worker-2] launch A
    

    根据日志可以看出 GlobalScope 不会阻塞当前线程。

    5.2、runBlocking

    runBlocking 函数的第二个参数被声明为 CoroutineScope 的扩展函数,所以在其内部就可以直接启动协程。

    public fun <T> runBlocking(
        context: CoroutineContext = EmptyCoroutineContext, 
        block: suspend CoroutineScope.() -> T
    ): T {
        // ...
    }
    

    runBlocking 示例:

    fun startRunBlocking() {
        log("start")
        runBlocking {
            launch {
                repeat(3) {
                    delay(100)
                    log("A $it")
                }
            }
    
            launch {
                repeat(3) {
                    delay(100)
                    log("B $it")
                }
            }
    
            GlobalScope.launch {
                repeat(3) {
                    delay(120)
                    log("GlobalScope $it")
                }
            }
        }
        log("end")
    }
    
    xxx 17:36:58.256 7142-7142/xxx D/qqq: [main] start
    xxx 17:36:58.365 7142-7142/xxx D/qqq: [main] A 0
    xxx 17:36:58.366 7142-7142/xxx D/qqq: [main] B 0
    xxx 17:36:58.385 7142-7326/xxx D/qqq: [DefaultDispatcher-worker-2] GlobalScope 0
    xxx 17:36:58.467 7142-7142/xxx D/qqq: [main] A 1
    xxx 17:36:58.468 7142-7142/xxx D/qqq: [main] B 1
    xxx 17:36:58.507 7142-7326/xxx D/qqq: [DefaultDispatcher-worker-2] GlobalScope 1
    xxx 17:36:58.569 7142-7142/xxx D/qqq: [main] A 2
    xxx 17:36:58.570 7142-7142/xxx D/qqq: [main] B 2
    xxx 17:36:58.571 7142-7142/xxx D/qqq: [main] end
    xxx 17:36:58.628 7142-7326/xxx D/qqq: [DefaultDispatcher-worker-2] GlobalScope 2
    

    根据日志可以看出,runBlocking 会阻塞当前线程,但其内部是非阻塞的,当内部相同作用域的所有协程都运行结束后,才会执行 runBlocking 后面的代码。

    5.3、coroutineScope

    用于创建一个独立的协程作用域,直到开启的协程任务全部完成后才结束自身,和 runBlocking 的区别在于,coroutineScope 不阻塞线程,且它是一个挂起函数。

    public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
        ...
    }
    

    5.4、supervisorScope

    用于创建一个使用了 SupervisorJob 的 coroutineScope,该作用域的特点是抛出的异常不会连锁取消同级协程和父协程。

    /**
     * Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend block with this scope.
     * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
     * context's [Job] with [SupervisorJob].
     *
     * A failure of a child does not cause this scope to fail and does not affect its other children,
     * so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for details.
     * A failure of the scope itself (exception thrown in the [block] or cancellation) fails the scope with all its children,
     * but does not cancel parent job.
     */
    public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
        ...
    }
    

    6、suspend(挂起)

    如果把之前例子中的 delay() 函数移动到 GlobalScope 外面调用的话,会发现代码错误:Suspend function 'delay' should be called only from a coroutine or another suspend function。意为 delay() 函数是一个挂起函数,只能由协程或者由其他挂起函数来调用,看看 delay() 的源码可见,函数前多了 suspend 修饰符。

    public suspend fun delay(timeMillis: Long) {
        if (timeMillis <= 0) return // don't delay
        return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
            cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
        }
    }
    

    聊到协程的非阻塞特性,往往都离不开 suspend 这个概念,究竟是怎么实现非阻塞的呢,这里涉及到两个概念:挂起和恢复。

    在我们使用 Thread.sleep() 的时候,代码执行到这一行,就休眠不在继续执行了,会等待休眠结束后继续执行。

    线程阻塞.gif

    如果使用协程做呢,可以发现日志是交替执行的,并没有发生阻塞。

    协程中存在一个类似调度中心的东西,在协程执行时,调度中心会将协程挂起,不阻碍后续的任务执行,在特定的时候,再回来继续执行。提高了利用率。

    协程挂起.gif

    7、CoroutineContext(协程上下文)

    协程上下文常用的几个实现类:DispatchersCoroutineNameCoroutineExceptionHandler

    7.1、Dispatchers(协程调度器)

    Dispatchers 用于指定协程的目标载体,即运行在哪个线程上。Kotlin 提供了四个 Dispatcher:

    • Dispatchers.Default 默认调度器,适合用于执行占用大量 CPU 资源的任务。例如:对列表排序和解析 JSON。
    • Dispatchers.IO 适合用于执行磁盘或网络 I/O 的任务。例如:使用 Room 组件、读写磁盘文件,执行网络请求。
    • Dispatchers.Unconfined 对执行协程的线程不做限制,可以直接在当前调度器所在线程上执行。
    • Dispatchers.Main 使用此调度程序可用于在 Android 主线程上运行协程,只能用于与界面交互和执行快速工作,例如:更新 UI、调用 LiveData.setValue。

    7.2、CoroutineName(协程命名)

    如果一个协程中有多个子协程,如果你想知道谁是谁,就可以通过 CoroutineName 来命名,使用如下:

    fun main(){
        val coroutineName = CoroutineName("MyCoroutine")
        GlobalScope.launch(coroutineName) {
            log("start ${coroutineName.name}")
        }
    }
    

    7.3、CoroutineExceptionHandler(协程异常捕捉)

    可以通过传入一个异常捕捉的上下文,将协程中出现的异常统一抛出来进行处理。

    fun main() {
        val exceptionHandler = CoroutineExceptionHandler { _, exception ->
            myPrint("exception: ${exception.message}")
        }
    
        GlobalScope.launch(exceptionHandler) {
            throw RuntimeException("test exception")
        }
    }
    

    7.4、组合上下文

    上面介绍了协程常用的一些上下文实现类,如果我想同时拥有怎么办?协程提供了+运算符来组合上下文。

    fun main() {
        val coroutineName = CoroutineName("MyCoroutine")
        
        val exceptionHandler = CoroutineExceptionHandler { _, exception ->
            myPrint("exception: ${exception.message}")
        }
    
        GlobalScope.launch(coroutineName + exceptionHandler) {
            throw RuntimeException("test exception")
        }
    }
    

    8、CoroutineBuilder(协程构建器)

    8.1、launch

    以下为launch 数的源代码,它是一个作用于 CoroutineScope 的扩展函数,用于在不阻塞当前线程的情况下启动一个协程,并返回对该协程的引用 Job 对象。

    public fun CoroutineScope.launch(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> Unit
    ): Job {
        ...
    }
    
    

    launch 数共包含三个参数:

    • context 用于指定协程的上下文。
    • start 用于指定协程的启动方式。
    • block。用于传递协程的执行体。

    8.2、async

    async 也是一个作用于 CoroutineScope 的扩展函数,和 launch 的区别主要就在于,async 可以返回协程的执行结果,而 launch 不行,可以看到 async 返回的是一个 Deferred 对象。

    public fun <T> CoroutineScope.async(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> T
    ): Deferred<T> {
        ...
    }
    

    通过 await() 方法可以拿到 async 协程的执行结果。

    9、参考

    一文快速入门 Kotlin 协程
    协程粉碎计划 | 协程到底是什么

    相关文章

      网友评论

          本文标题:Kotlin 协程基本概念

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