1 它是什么(协程 和 Kotlin协程)

1.1 协程是什么

维基百科:协程,英文Coroutine [kəru’tin] (可入厅),是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复。

作为Google钦定的Android开发首选语言Kotlin,协程并不是 Kotlin 提出来的新概念,目前有协程概念的编程语言有Lua语言、Python语言、Go语言、C语言等,它只是一种编程思想,不局限于特定的语言。

而每一种编程语言中的协程的概念及实现又不完全一样,本次分享主要讲Kotlin协程。

1.2 Kotlin协程是什么

Kotlin官网:协程是轻量级线程

可简单理解:一个线程框架,是全新的处理并发的方式,也是Android上方便简化异步执行代码的方式

类似于 Java:线程池 Android:Handler和AsyncTask,RxJava的Schedulers

注:Kotlin不仅仅是面向JVM平台的,还有JS/Native,如果用kotlin来写前端,那Koltin的协程就是JS意义上的协程。如果仅仅JVM 平台,那确实应该是线程框架。

1.3 进程、线程、协程比较

可通过以下两张图理解三者的不同和关系

2 为什么选择它(协程解决什么问题)

异步场景举例:

  1. 第一步:接口获取当前用户token及用户信息
  2. 第二步:将用户的昵称展示界面上
  3. 第三步:然后再通过这个token获取当前用户的消息未读数
  4. 第四步:并展示在界面上

2.1 现有方案实现

apiService.getUserInfo().enqueue(object :Callback<User>{
override fun onResponse(call: Call<User>, response: Response<User>) {
val user = response.body()
tvNickName.text = user?.nickName
apiService.getUnReadMsgCount(user?.token).enqueue(object :Callback<Int>{
override fun onResponse(call: Call<Int>, response: Response<Int>) {
val tvUnReadMsgCount = response.body()
tvMsgCount.text = tvUnReadMsgCount.toString()
}
})
}
})

现有方案如何拿到异步任务的数据,得不到就毁掉哈哈哈,就是通过回调函数来解决。

若嵌套多了,这种画风是不是有点回调地狱的感觉,俗称的「callback hell」

2.2 协程实现

mainScope.launch {
val user = apiService.getUserInfoSuspend() //IO线程请求数据
tvNickName.text = user?.nickName //UI线程更新界面
val unReadMsgCount = apiService.getUnReadMsgCountSuspend(user?.token) //IO线程请求数据
tvMsgCount.text = unReadMsgCount.toString() //UI线程更新界面
}
suspend fun getUserInfoSuspend() :User? {
return withContext(Dispatchers.IO){
//模拟网络请求耗时操作
delay(10)
User("asd123", "userName", "nickName")
}
} suspend fun getUnReadMsgCountSuspend(token:String?) :Int{
return withContext(Dispatchers.IO){
//模拟网络请求耗时操作
delay(10)
10
}
}

红色框框内的就是一个协程代码块。

可以看得出在协程实现中告别了callback,所以再也不会出现回调地狱这种情况了,协程解决了回调地狱

协程可以让我们用同步的代码写出异步的效果,这也是协程最大的优势,异步代码同步去写。

小结:协程可以异步代码同步去写,解决回调地狱,让程序员更方便地处理异步业务,更方便地切线程,保证主线程安全。

它是怎么做到的?

3 它是怎么工作的(协程的原理浅析)

3.1 协程的挂起和恢复

挂起(非阻塞式挂起)

suspend 关键字,它是协程中核心的关键字,是挂起的标识。

下面看一下上述示例代码切换线程的过程:

每一次从主线程切到IO线程都是一次协程的挂起操作;

每一次从IO线程切换主线程都是一次协程的恢复操作;

挂起和恢复是suspend函数特有的能力,其他函数不具备,挂起的内容是协程,不是挂起线程,也不是挂起函数,当线程执行到suspend函数的地方,不会继续执行当前协程的代码了,所以它不会阻塞线程,是非阻塞式挂起。

有挂起必然有恢复流程, 恢复是指将已经被挂起的目标协程从挂起之处开始恢复执行。在协程中,挂起和恢复都不需要我们手动处理,这些都是kotlin协程帮我们自动完成的。

那Kotlin协程是如何帮我们自动实现挂起和恢复操作的呢?

它是通过Continuation来实现的。 [kənˌtɪnjuˈeɪʃ(ə)n] (继续;延续;连续性;后续部分)

3.2 协程的挂起和恢复的工作原理(Continuation)

CPS + 状态机

Java中没有suspend函数,suspend是Kotlin中特有的关键字,当编译时,Kotlin编译器会将含有suspend关键字的函数进行一次转换。

这种被编译器转换在kotlin中叫CPS转换(cotinuation-passing-style)。

转换流程如下所示

程序员写的挂起函数代码:

suspend fun getUserInfo() : User {
val user = User("asd123", "userName", "nickName")
return user
}

假想的一种中间态代码(便于理解):

fun getUserInfo(callback: Callback<User>): Any? {
val user = User("asd123", "userName", "nickName")
callback.onSuccess(user)
return Unit
}

转换后的代码:

fun getUserInfo(cont: Continuation<User>): Any? {
val user = User("asd123", "userName", "nickName")
cont.resume(user)
return Unit
}

我们通过Kotlin生成字节码工具查看字节码,然后将其反编译成Java代码:

@Nullable
public final Object getUserInfo(@NotNull Continuation $completion) {
User user = new User("asd123", "userName", "nickName");
return user;
}

这也验证了确实是会通过引入一个Continuation对象来实现恢复的流程,这里的这个Continuation对象中包含了Callback的形态

它有两个作用:1. 暂停并记住执行点位;2. 记住函数暂停时刻的局部变量上下文。

所以为什么我们可以用同步的方式写异步代码,是因为Continuation帮我们做了回调的流程。

下面看一下这个Continuation 的源码部分

可以看到这个Continuation中封装了一个resumeWith的方法,这个方法就是恢复用的。

internal abstract class BaseContinuationImpl() : Continuation<Any?> {

    public final override fun resumeWith(result: Result<Any?>) {
//省略好多代码
invokeSuspend()
//省略好多代码
} protected abstract fun invokeSuspend(result: Result<Any?>): Any?
} internal abstract class ContinuationImpl(
completion: Continuation<Any?>?,
private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) { protected abstract fun invokeSuspend(result: Result<Any?>): Any? //invokeSuspend() 这个方法是恢复的关键一步

继续看上述例子:

这是一个CPS之前的代码:

suspend fun testCoroutine() {
val user = apiService.getUserInfoSuspend() //挂起函数 IO线程
tvNickName.text = user?.nickName //UI线程更新界面
val unReadMsgCount = apiService.getUnReadMsgCountSuspend(user?.token) //挂起函数 IO线程
tvMsgCount.text = unReadMsgCount.toString() //UI线程更新界面
}

当前挂起函数里有两个挂起函数

通过kotlin编译器编译后:

fun testCoroutine(completion: Continuation<Any?>): Any? {
// TestContinuation本质上是匿名内部类
class TestContinuation(completion: Continuation<Any?>?) : ContinuationImpl(completion) {
// 表示协程状态机当前的状态
var label: Int = 0 // 两个变量,对应原函数的2个变量
lateinit var user: Any
lateinit var unReadMsgCount: Int // result 接收协程的运行结果
var result = continuation.result // suspendReturn 接收挂起函数的返回值
var suspendReturn: Any? = null // CoroutineSingletons 是个枚举类
// COROUTINE_SUSPENDED 代表当前函数被挂起了
val sFlag = CoroutineSingletons.COROUTINE_SUSPENDED // invokeSuspend 是协程的关键
// 它最终会调用 testCoroutine(this) 开启协程状态机
// 状态机相关代码就是后面的 when 语句
// 协程的本质,可以说就是 CPS + 状态机
override fun invokeSuspend(_result: Result<Any?>): Any? {
result = _result
label = label or Int.Companion.MIN_VALUE
return testCoroutine(this)
}
} // ...
val continuation = if (completion is TestContinuation) {
completion
} else {
// 作为参数
// ↓
TestContinuation(completion)
loop = true
while(loop) {
when (continuation.label) {
0 -> {
// 检测异常
throwOnFailure(result) // 将 label 置为 1,准备进入下一次状态
continuation.label = 1 // 执行 getUserInfoSuspend(第一个挂起函数)
suspendReturn = getUserInfoSuspend(continuation) // 判断是否挂起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
} 1 -> {
throwOnFailure(result) // 获取 user 值
user = result as Any // 准备进入下一个状态
continuation.label = 2 // 执行 getUnReadMsgCountSuspend
suspendReturn = getUnReadMsgCountSuspend(user.token, continuation) // 判断是否挂起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
} 2 -> {
throwOnFailure(result) user = continuation.mUser as Any
unReadMsgCount = continuation.unReadMsgCount as Int
loop = false
}
}

通过一个label标签控制分支代码执行,label为0,首先会进入第一个分支,首先将label设置为下一个分支的数值,然后执行第一个suspend方法并传递当前Continuation,得到返回值,如果是COROUTINE SUSPENDED,协程框架就直接return,协程挂起,当第一个suspend方法执行完成,会回调Continuation的invokeSuspend方法,进入第二个分支执行,以此类推执行完所有suspend方法。

每一个挂起点和初始挂起点对应的 Continuation 都会转化为一种状态,协程恢复只是跳转到下一种状态中。挂起函数将执行过程分为多个 Continuation 片段,并且利用状态机的方式保证各个片段是顺序执行的。

小结:协程的挂起和恢复的本质是CPS + 状态机

4 总结

总结几个不用协程实现起来很麻烦的骚操作:

  1. 如果有一个函数,它的返回值需要等到多个耗时的异步任务都执行完毕返回之后,组合所有任务的返回值作为 最终返回值
  2. 如果有一个函数,需要顺序执行多个网络请求,并且后一个请求依赖前一个请求的执行结果
  3. 当前正在执行一项异步任务,但是你突然不想要它执行了,随时可以取消
  4. 如果你想让一个任务最多执行3秒,超过3秒则自动取消

Kotlin协程之所以被认为是假协程,是因为它并不在同一个线程运行,而是真的会创建多个线程。

Kotlin协程在Android上只是一个类似线程池的封装,真就是一个线程框架。但是它却可以让我们用同步的代码风格写出异步的效果,至于怎么做的,这个不需要我们操心,这些都是kotlin帮我们处理好了,我们需要关心的是怎么用好它

它就是一个线程框架。

作者:京东物流 王斌

来源:京东云开发者社区 自猿其说Tech 转载请注明来源

Android Kotlin 协程初探的更多相关文章

  1. Android Kotlin协程入门

    Android官方推荐使用协程来处理异步问题.以下是协程的特点: 轻量:单个线程上可运行多个协程.协程支持挂起,不会使正在运行协程的线程阻塞.挂起比阻塞节省内存,且支持多个并行操作. 内存泄漏更少:使 ...

  2. Kotlin 协程一 —— 全面了解 Kotlin 协程

    一.协程的一些前置知识 1.1 进程和线程 1.1.1基本定义 1.1.2为什么要有线程 1.1.3 进程与线程的区别 1.2 协作式与抢占式 1.2.1 协作式 1.2.2 抢占式 1.3 协程 二 ...

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

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

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

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

  5. Retrofit使用Kotlin协程发送请求

    Retrofit2.6开始增加了对Kotlin协程的支持,可以通过suspend函数进行异步调用.本文简单介绍一下Retrofit中协程的使用 导入依赖 app的build文件中加入: impleme ...

  6. Kotlin协程基础

    开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 我们已经通过第一个例子学会了启 ...

  7. rxjava回调地狱-kotlin协程来帮忙

    本文探讨的是在tomcat服务端接口编程中, 异步servlet场景下( 参考我另外一个文章),用rxjava来改造接口为全流程异步方式 好处不用说 tomcat的worker线程利用率大幅提高,接口 ...

  8. Kotlin协程作用域与构建器详解

    在上次我们是通过了这种方式来创建了一个协程: 接着再来看另一种创建协程的方式: 下面用它来实现上一次程序一样的效果,先来回顾一下上一次程序的代码: 好,下面改用runBlocking的方式: 运行一下 ...

  9. Kotlin协程通信机制: Channel

    Coroutines Channels Java中的多线程通信, 总会涉及到共享状态(shared mutable state)的读写, 有同步, 死锁等问题要处理. 协程中的Channel用于协程间 ...

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

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

随机推荐

  1. 运行C时报错:relocation truncated to fit: R_X86_64_PC32 against undefined symbol `WinMain‘ collect2: error

    写C时,遇到报错 [Running] cd "d:\考研\408\LeranC\Code\GramForC\" && gcc 01data_types.c -o 0 ...

  2. Java扩展Nginx之四:远程调试

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本文是<Java扩展Nginx> ...

  3. T-star高校挑战赛部分wp

    web-1签到 checkin还是很基础的 网站上传存在js检测,禁用js即可上传 写PHP一句话木马上传,http://588f25a5.yunyansec.com/upload/test.php连 ...

  4. 利用shell脚本交互式运行jar任务

    如题,废话不多说,直接上代码: #!/bin/bash APP_PATH=/root/bigdata/neural_networks/width_control_model/predict/uploa ...

  5. Ubuntu 20.04使用 VNC远程桌面连接避坑指南

    Ubuntu 20.04使用 VNC远程桌面连接避坑指南 自从开始使用Ubuntu 20.04搭建深度学习服务器,就想到使用VNC远程桌面连接使用.可是之前一直使用的是Ubuntu18.04,心里想着 ...

  6. Linux设置字符编码

    一.Linux设置字符编码 1.什么是字符编码 字符编码可以实现对非英文字符的支持,防止非英文字符的乱码. 2.国内常用的字符编码 UTF-8 GBK 3.设置字符编码 我们可以对Linux系统的字符 ...

  7. jenkins打包报错的排查思路与解决

    背景 废话少说, 在新建一个jenkins流水线时, 碰到了打包死活无法成功的问题, 相关配置如下图 运行后最后的日志如图 定位问题 通过查看日志, 发现报错的模块是构建后执行shell的时候, 但是 ...

  8. 绕过PHP执行命令的函数执行系统cmd命令

    <?php $evil=`set`; echo '<pre>'.$evil.'</pre>'; ?> 成功执行set命令,你懂的! 顺手写了个php执行cmd命令的 ...

  9. mac一键获取最新datagrid 2017.3注册码到剪贴板

    mac一键获取最新datagrid 2017.3注册码到剪贴板 近期datagrid 校验激活码合法性的频率提高,导致需要频繁输入激活码 遂整理一脚本,自动获取最新注册码,并拷贝到剪贴板. 打开dat ...

  10. [故障处理]nfs导致系统负载异常

    目录 情况 排查 原因 解决 情况 某台虚拟机服务器系统负载极高,但是cpu.内存.IO都正常.home目录下无法使用ls,也无法使用 df -h. 排查 top看cpu和内存,正常. iotop看i ...