Kotlin 协程基本概念

2022-09-05  本文已影响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 来执行耗时任务,但实际上协程和线程有着本质上的区别。通过使用协程,可以极大的提高线程的并发效率,避免以往嵌套的回调地狱,极大的提高了代码的可读性。

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

5、CoroutineScope(作用域)

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

CoroutineScope 大体上可以分为三种:

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:

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 数共包含三个参数:

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 协程
协程粉碎计划 | 协程到底是什么

上一篇下一篇

猜你喜欢

热点阅读