Coroutines 协程

最近在总结Kotlin的一些东西, 发现协程这块确实不容易说清楚. 之前的那篇就写得不好, 所以决定重写.

反复研究了官网文档和各种教程博客, 本篇内容是最基础也最主要的内容, 力求小白也能看懂并理解.

本文被收录在: https://github.com/mengdd/KotlinTutorials

Coroutines概念

Coroutines(协程), 计算机程序组件, 通过允许任务挂起和恢复执行, 来支持非抢占式的多任务. (见Wiki).

协程主要是为了异步, 非阻塞的代码. 这个概念并不是Kotlin特有的, Go, Python等多个语言中都有支持.

Kotlin Coroutines

Kotlin中用协程来做异步和非阻塞任务, 主要优点是代码可读性好, 不用回调函数. (用协程写的异步代码乍一看很像同步代码.)

Kotlin对协程的支持是在语言级别的, 在标准库中只提供了最低程度的APIs, 然后把很多功能都代理到库中.

Kotlin中只加了suspend作为关键字.

asyncawait不是Kotlin的关键字, 也不是标准库的一部分.

比起futures和promises, kotlin中suspending function的概念为异步操作提供了一种更安全和不易出错的抽象.

kotlinx.coroutines是协程的库, 为了使用它的核心功能, 项目需要增加kotlinx-coroutines-core的依赖.

Coroutines Basics: 协程到底是什么?

先上一段官方的demo:

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch fun main() {
GlobalScope.launch { // launch a new coroutine in background and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello,") // main thread continues while coroutine is delayed
Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

这段代码的输出:

先打印Hello, 延迟1s之后, 打印World.

对这段代码的解释:

launch开始了一个计算, 这个计算是可挂起的(suspendable), 它在计算过程中, 释放了底层的线程, 当协程执行完成, 就会恢复(resume).

这种可挂起的计算就叫做一个协程(coroutine). 所以我们可以简单地说launch开始了一个新的协程.

注意, 主线程需要等待协程结束, 如果注释掉最后一行的Thread.sleep(2000L), 则只打印Hello, 没有World.

协程和线程的关系

coroutine(协程)可以理解为轻量级的线程. 多个协程可以并行运行, 互相等待, 互相通信. 协程和线程的最大区别就是协程非常轻量(cheap), 我们可以创建成千上万个协程而不必考虑性能.

协程是运行在线程上可以被挂起的运算. 可以被挂起, 意味着运算可以被暂停, 从线程移除, 存储在内存里. 此时, 线程就可以自由做其他事情. 当计算准备好继续进行时, 它会返回线程(但不一定要是同一个线程).

默认情况下, 协程运行在一个共享的线程池里, 线程还是存在的, 只是一个线程可以运行多个协程, 所以线程没必要太多.

调试

在上面的代码中加上线程的名字:

fun main() {
GlobalScope.launch {
// launch a new coroutine in background and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World! + ${Thread.currentThread().name}") // print after delay
}
println("Hello, + ${Thread.currentThread().name}") // main thread continues while coroutine is delayed
Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

可以在IDE的Edit Configurations中设置VM options: -Dkotlinx.coroutines.debug, 运行程序, 会在log中打印出代码运行的协程信息:

Hello, + main
World! + DefaultDispatcher-worker-1 @coroutine#1

suspend function

上面例子中的delay方法是一个suspend function.

delay()Thread.sleep()的区别是: delay()方法可以在不阻塞线程的情况下延迟协程. (It doesn't block a thread, but only suspends the coroutine itself). 而Thread.sleep()则阻塞了当前线程.

所以, suspend的意思就是协程作用域被挂起了, 但是当前线程中协程作用域之外的代码不被阻塞.

如果把GlobalScope.launch替换为thread, delay方法下面会出现红线报错:

Suspend functions are only allowed to be called from a coroutine or another suspend function

suspend方法只能在协程或者另一个suspend方法中被调用.

在协程等待的过程中, 线程会返回线程池, 当协程等待结束, 协程会在线程池中一个空闲的线程上恢复. (The thread is returned to the pool while the coroutine is waiting, and when the waiting is done, the coroutine resumes on a free thread in the pool.)

启动协程

启动一个新的协程, 常用的主要有以下几种方式:

  • launch
  • async
  • runBlocking

它们被称为coroutine builders. 不同的库可以定义其他更多的构建方式.

runBlocking: 连接blocking和non-blocking的世界

runBlocking用来连接阻塞和非阻塞的世界.

runBlocking可以建立一个阻塞当前线程的协程. 所以它主要被用来在main函数中或者测试中使用, 作为连接函数.

比如前面的例子可以改写成:

fun main() = runBlocking<Unit> {
// start main coroutine
GlobalScope.launch {
// launch a new coroutine in background and continue
delay(1000L)
println("World! + ${Thread.currentThread().name}")
}
println("Hello, + ${Thread.currentThread().name}") // main coroutine continues here immediately
delay(2000L) // delaying for 2 seconds to keep JVM alive
}

最后不再使用Thread.sleep(), 使用delay()就可以了.

程序输出:

Hello, + main @coroutine#1
World! + DefaultDispatcher-worker-1 @coroutine#2

launch: 返回Job

上面的例子delay了一段时间来等待一个协程结束, 不是一个好的方法.

launch返回Job, 代表一个协程, 我们可以用Jobjoin()方法来显式地等待这个协程结束:

fun main() = runBlocking {
val job = GlobalScope.launch {
// launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World! + ${Thread.currentThread().name}")
}
println("Hello, + ${Thread.currentThread().name}")
job.join() // wait until child coroutine completes
}

输出结果和上面是一样的.

Job还有一个重要的用途是cancel(), 用于取消不再需要的协程任务.

async: 从协程返回值

async开启线程, 返回Deferred<T>, Deferred<T>Job的子类, 有一个await()函数, 可以返回协程的结果.

await()也是suspend函数, 只能在协程之内调用.

fun main() = runBlocking {
// @coroutine#1
println(Thread.currentThread().name)
val deferred: Deferred<Int> = async {
// @coroutine#2
loadData()
}
println("waiting..." + Thread.currentThread().name)
println(deferred.await()) // suspend @coroutine#1
} suspend fun loadData(): Int {
println("loading..." + Thread.currentThread().name)
delay(1000L) // suspend @coroutine#2
println("loaded!" + Thread.currentThread().name)
return 42
}

运行结果:

main @coroutine#1
waiting...main @coroutine#1
loading...main @coroutine#2
loaded!main @coroutine#2
42

Context, Dispatcher和Scope

看一下launch方法的声明:

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

其中有几个相关概念我们要了解一下.

协程总是在一个context下运行, 类型是接口CoroutineContext. 协程的context是一个索引集合, 其中包含各种元素, 重要元素就有Job和dispatcher. Job代表了这个协程, 那么dispatcher是做什么的呢?

构建协程的coroutine builder: launch, async, 都是CoroutineScope类型的扩展方法. 查看CoroutineScope接口, 其中含有CoroutineContext的引用. scope是什么? 有什么作用呢?

下面我们就来回答这些问题.

Dispatchers和线程

Context中的CoroutineDispatcher可以指定协程运行在什么线程上. 可以是一个指定的线程, 线程池, 或者不限.

看一个例子:

fun main() = runBlocking<Unit> {
launch {
// context of the parent, main runBlocking coroutine
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) {
// not confined -- will work with main thread
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) {
// will get dispatched to DefaultDispatcher
println("Default : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) {
// will get its own new thread
println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
}

运行后打印出:

Unconfined            : I'm working in thread main
Default : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking : I'm working in thread main

API提供了几种选项:

  • Dispatchers.Default代表使用JVM上的共享线程池, 其大小由CPU核数决定, 不过即便是单核也有两个线程. 通常用来做CPU密集型工作, 比如排序或复杂计算等.
  • Dispatchers.Main指定主线程, 用来做UI更新相关的事情. (需要添加依赖, 比如kotlinx-coroutines-android.) 如果我们在主线程上启动一个新的协程时, 主线程忙碌, 这个协程也会被挂起, 仅当线程有空时会被恢复执行.
  • Dispatchers.IO: 采用on-demand创建的线程池, 用于网络或者是读写文件的工作.
  • Dispatchers.Unconfined: 不指定特定线程, 这是一个特殊的dispatcher.

如果不明确指定dispatcher, 协程将会继承它被启动的那个scope的context(其中包含了dispatcher).

在实践中, 更推荐使用外部scope的dispatcher, 由调用方决定上下文. 这样也方便测试.

newSingleThreadContext创建了一个线程来跑协程, 一个专注的线程算是一种昂贵的资源, 在实际的应用中需要被释放或者存储复用.

切换线程还可以用withContext, 可以在指定的协程context下运行代码, 挂起直到它结束, 返回结果.

另一种方式是新启一个协程, 然后用join明确地挂起等待.

在Android这种UI应用中, 比较常见的做法是, 顶部协程用CoroutineDispatchers.Main, 当需要在别的线程上做一些事情的时候, 再明确指定一个不同的dispatcher.

Scope是什么?

launch, asyncrunBlocking开启新协程的时候, 它们自动创建相应的scope. 所有的这些方法都有一个带receiver的lambda参数, 默认的receiver类型是CoroutineScope.

IDE会提示this: CoroutineScope:

launch { /* this: CoroutineScope */
}

当我们在runBlocking, launch, 或async的大括号里面再创建一个新的协程的时候, 自动就在这个scope里创建:

fun main() = runBlocking {
/* this: CoroutineScope */
launch { /* ... */ }
// the same as:
this.launch { /* ... */ }
}

因为launch是一个扩展方法, 所以上面例子中默认的receiver是this.

这个例子中launch所启动的协程被称作外部协程(runBlocking启动的协程)的child. 这种"parent-child"的关系通过scope传递: child在parent的scope中启动.

协程的父子关系:

  • 当一个协程在另一个协程的scope中被启动时, 自动继承其context, 并且新协程的Job会作为父协程Job的child.

所以, 关于scope目前有两个关键知识点:

  • 我们开启一个协程的时候, 总是在一个CoroutineScope里.
  • Scope用来管理不同协程之间的父子关系和结构.

协程的父子关系有以下两个特性:

  • 父协程被取消时, 所有的子协程都被取消.
  • 父协程永远会等待所有的子协程结束.

值得注意的是, 也可以不启动协程就创建一个新的scope. 创建scope可以用工厂方法: MainScope()CoroutineScope().

coroutineScope()方法也可以创建scope. 当我们需要以结构化的方式在suspend函数内部启动新的协程, 我们创建的新的scope, 自动成为suspend函数被调用的外部scope的child.

上面的父子关系, 可以进一步抽象到, 没有parent协程, 由scope来管理其中所有的子协程.

Scope在实际应用中解决什么问题呢? 如果我们的应用中, 有一个对象是有自己的生命周期的, 但是这个对象又不是协程, 比如Android应用中的Activity, 其中启动了一些协程来做异步操作, 更新数据等, 当Activity被销毁的时候需要取消所有的协程, 来避免内存泄漏. 我们就可以利用CoroutineScope来做这件事: 创建一个CoroutineScope对象和activity的生命周期绑定, 或者让activity实现CoroutineScope接口.

所以, scope的主要作用就是记录所有的协程, 并且可以取消它们.

A CoroutineScope keeps track of all your coroutines, and it can cancel all of the coroutines started in it.

Structured Concurrency

这种利用scope将协程结构化组织起来的机制, 被称为"structured concurrency".

好处是:

  • scope自动负责子协程, 子协程的生命和scope绑定.
  • scope可以自动取消所有的子协程.
  • scope自动等待所有的子协程结束. 如果scope和一个parent协程绑定, 父协程会等待这个scope中所有的子协程完成.

通过这种结构化的并发模式: 我们可以在创建top级别的协程时, 指定主要的context一次, 所有嵌套的协程会自动继承这个context, 只在有需要的时候进行修改即可.

GlobalScope: daemon

GlobalScope启动的协程都是独立的, 它们的生命只受到application的限制. 即GlobalScope启动的协程没有parent, 和它被启动时所在的外部的scope没有关系.

launch(Dispatchers.Default) { ... }GlobalScope.launch { ... }用的dispatcher是一样的.

GlobalScope启动的协程并不会保持进程活跃. 它们就像daemon threads(守护线程)一样, 如果JVM发现没有其他一般的线程, 就会关闭.

参考

第三方博客:

欢迎关注微信公众号: 圣骑士Wind

Kotlin Coroutines不复杂, 我来帮你理一理的更多相关文章

  1. Kotlin Coroutines在Android中的实践

    Coroutines在Android中的实践 前面两篇文章讲了协程的基础知识和协程的通信. 见: Kotlin Coroutines不复杂, 我来帮你理一理 Kotlin协程通信机制: Channel ...

  2. 探究高级的Kotlin Coroutines知识

    要说程序如何从简单走向复杂, 线程的引入必然功不可没, 当我们期望利用线程来提升程序效能的过程中, 处理线程的方式也发生了从原始时代向科技时代发生了一步一步的进化, 正如我们的Elisha大神所著文章 ...

  3. 带你深入理解STL之空间配置器(思维导图+源码)

    前不久把STL细看了一遍,由于看得太"认真",忘了做笔记,归纳和总结这步漏掉了.于是为了加深印象,打算重看一遍,并记录下来里面的一些实现细节.方便以后能较好的复习它. 以前在项目中 ...

  4. java 中 printf()语句的理解

    对print和println的理解很简单,今天突然接触到printf(),有点懵,整理了下也帮自己理一理 printf是格式化输出的形式 下在举个例子: package other; public c ...

  5. Kotlin高阶函数实战

    前言 1. 高阶函数有多重要? 高阶函数,在 Kotlin 里有着举足轻重的地位.它是 Kotlin 函数式编程的基石,它是各种框架的关键元素,比如:协程,Jetpack Compose,Gradle ...

  6. Kotlin协程解析系列(上):协程调度与挂起

    vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...

  7. Kotlin协程作用域与Job详解

    Job详解: 在上一次https://www.cnblogs.com/webor2006/p/11725866.html中抛出了一个问题: 所以咱们将delay去掉,需要改造一下,先把主线程的dela ...

  8. Kotlin协程第一个示例剖析及Kotlin线程使用技巧

    Kotlin协程第一个示例剖析: 上一次https://www.cnblogs.com/webor2006/p/11712521.html已经对Kotlin中的协程有了理论化的了解了,这次则用代码来直 ...

  9. Kotlin DSL for HTML实例解析

    Kotlin DSL for HTML实例解析 Kotlin DSL, 指用Kotlin写的Domain Specific Language. 本文通过解析官方的Kotlin DSL写html的例子, ...

随机推荐

  1. ‎Cocos2d-x 学习笔记(26) 从源码学习 DrawCall 的降低方法

    [Cocos2d-x]学习笔记目录 本文链接:https://www.cnblogs.com/deepcho/cocos2dx-drawcall-glcalls 1. 屏幕左下角 我们通常在Cocos ...

  2. java-try,return和finally相遇时的各种情况

    今天碰到了这样一个问题:使用try,return和finally会碰到的各种情况1,try中有return时,执行顺序:2,try和finally中都有return时,执行顺序:3,运算代码在fina ...

  3. find命令面试题

    注意 (1)建议先创建快照 (2)有可能存在命令正确,但是查找不到文件的情况,是因为不存在相关条件的文件 (3)如果存在命令正确,但是查找不到文件的情况,则先创建相关的文件.目录.用户.组,设置好对应 ...

  4. OpenvSwitch系列之ovs-vsctl命令使用

    Open vSwitch系列之一 Open vSwitch诞生 Open vSwitch系列之二 安装指定版本ovs Open vSwitch系列之三 ovs-vsctl 命令使用 OpenvSwit ...

  5. 百万年薪python之路 -- 函数的动态参数练习

    1.继续整理函数相关知识点. 2.写函数,接收n个数字,求这些参数数字的和.(动态传参) def func(*args,**kwargs): num_sum = 0 num_dic = [] num ...

  6. 如何在 GitHub 的项目中创建一个分支呢?

    如何在 GitHub 的项目中创建一个分支呢? 其实很简单啦,直接点击 Branch,然后在弹出的文本框中添加自己的 Branch Name 然后点击蓝色的Create branch就可以了,这样一来 ...

  7. Shiro learning - 入门学习 Shiro中的基础知识(1)

    Shiro入门学习 一 .什么是Shiro? 看一下官网对于 what is Shiro ? 的解释 Apache Shiro (pronounced “shee-roh”, the Japanese ...

  8. OTA升级详解(一)

    不积跬步,无以至千里: 不积小流,无以成江海. 出自荀子<劝学篇> 1.概念解释 OTA是何物? 英文解释为 Over The Air,既空中下载的意思,具体指远程无线方式,OTA 技术可 ...

  9. (三)Kinect姿势识别

    Kinect给我们内置了许多姿势如举手等,具体可参考枚举KinectGestures.Gestures,也可以通过Kinect姿势管理器,自定义姿势导入(坑较多,内置的基本够用了)也可以根据关节坐标自 ...

  10. Linux文件同步工具之rsync

    学习背景 1.最近公司的项目在使用jenkins做自动化构建,因为jenkins在构建时是比较耗性能的,便单独使用了一台服务器做构建服务器.但是个人觉得这样成本过高,单独拿一台服务器来构建并且该服务器 ...