Kotlin 协程基本概念

目录

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

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

推荐阅读更多精彩内容

  • [TOC] 简介 Coroutines are computer program components that ...
    Whyn阅读 5,931评论 5 15
  • 在今年的三月份,我因为需要为项目搭建一个新的网络请求框架开始接触 Kotlin 协程。那时我司项目中同时存在着两种...
    业志陈阅读 1,047评论 0 5
  • 一、Kotlin 协程概念 Kotlin 协程提供了一种全新处理并发的方式,你可以在 Android 平台上使用它...
    4e70992f13e7阅读 1,714评论 0 2
  • 为什么要搞出和用协程呢 是节省CPU,避免系统内核级的线程频繁切换,造成的CPU资源浪费。好钢用在刀刃上。而协程是...
    静默的小猫阅读 642评论 0 2
  • 在 Kotlin 中的变量、常量以及注释多多少少和 Java 语言是有着不同之处的。下面详细的介绍 Kotlin ...
    驰同学阅读 891评论 0 2