Kotlin协程-那些理不清乱不明的关系
Kotlin的协程自推出以来,受到了越来越多Android开发者的追捧。另一方面由于它庞大的API,也将相当一部分开发者拒之门外。本篇试图从协程的几个重要概念入手,在复杂API中还原出它本来的面目,以全新的角度带读者走进Kotlin协程世界。
什么是协程
在很多有关协程的文章中,描述协程通常会用这样的一句描述——协程比线程更加轻量,是可取消的。这句话没有错,这两个都是协程的优点,但是并不是特点,它并没有解释协程是什么。那么什么是协程的特点呢,我觉得可以先用线程做个类比,解释一个概念最好的办法就是类比。 我不打算使用科学严谨的描述,我想给线程一个我自己的定义——线程是一个可供CPU调度的执行单元,它有自己的执行块,可以独立地执行逻辑。我特意将线程的三个关键特征列举出来了:
- 线程由CPU调度
- 线程拥有自己的代码块
- 代码块需要才能调度执行
这是我对线程的直观感受,并且这些特点是能从代码上体现出来的。如Java中的Thread,它是线程的同时,也是一个实体对象,能通过API来认识它,使用它。而它的特别之处就是我上面列举的三点,这些都是线程特有的。我试着用相同的思路来给协程下一个自己的定义——协程是由线程调度执行的执行单元,它也有自己的执行块,可以独立或者协同执行逻辑。其实Kotlin中的协程也有自己对应实体和操作API,甚至和线程的还很像(如生命周期),但是由于Kotlin对协程的封装过于彻底,很多API没有暴露出来,以至于我对协程的认识一直处于盲人摸象的状态。另外,其实协程是有多种实现方式的,以下我的观点仅针对Kotlin的协程实现,可能与其他语言的实现不一致。
Kotlin中的协程对象本质上来讲就是个可执行的代码块, 执行的代码就是创建协程传递进去的。除此之外它还有个最大的特点——协程是不和线程绑定的。它可以在某个时刻断开当前的线程,然后在其他时候,附着到其他线程上。也就是说,它像一个乒乓球,线程就好比是球拍,它可以在球拍间反复横跳。所以,结合这两个特点,官方给协程的定义是 一个可挂起的计算实体。我觉得这个定义不算精确,它只体现了挂起这个概念,没有体现恢复的概念。我给它一个我自己的定义——一个可被调度的计算实体。
协程中几个关键概念
明白了协程是什么还不够,因为Kotlin的协程还涉及到很多方面,有几个关键概念需要理解。
挂起函数
提到Kotlin的协程就不得不提到挂起函数,这是Kotlin协程实现的最重要的概念之一。简单来说,挂起函数是一种异步实现方案,它是一个普通函数的前提下,还具备挂起和恢复的特性。 它是解决耗时计算和结果传递的一种方案。那么它和我们常见的基于回调的方案相比,有什么不同呢。在基于回调的方案中,计算过程和结果没有关系,结果需要通过另一个对象,在计算完成后由计算过程手动传递,而这一过程是可能被反复嵌套的,从而导致,一些本该是串行化的代码,被割裂成几个部分,分散到不同的代码块中。如以下读写文件的代码
// asynchronously read into `buf`, and when done run the lambda
inChannel.read(buf) {
// this lambda is executed when the reading completes
bytesRead ->
...
...
process(buf, bytesRead)
// asynchronously write from `buf`, and when done run the lambda
outChannel.write(buf) {
// this lambda is executed when the writing completes
...
...
outFile.close()
}
}
同样的逻辑,将read和write实现为挂起函数后,能写成什么样呢?
launch {
// suspend while asynchronously reading
val bytesRead = inChannel.aRead(buf)
// we only get to this line when reading completes
...
...
process(buf, bytesRead)
// suspend while asynchronously writing
outChannel.aWrite(buf)
// we only get to this line when writing completes
...
...
outFile.close()
}
这是Kotlin官方给的一个例子,可以看出挂起函数的实现非常符合直觉,是和思考过程保持一致的,同时还减少了大量的嵌套。
为了更好地解释挂起函数,我还需要引入了一个新的概念——挂起点。
挂起点是一个分界点,代表着从这个时刻之后,执行过程可能会转移到其他地方执行,然后在某个时刻,再从这个点恢复,继续往下执行。这个过程中,当前线程不会被阻塞。所以 挂起函数其实实现了异步非阻塞的通信模式。
一句话总结,挂起函数是一种不阻塞当前线程,并能返回异步计算结果的函数。
协程创建者
前面提到的挂起函数虽然好,但是有个限制,普通方法是不能调用挂起函数的,只能通过挂起函数调用。那么就出现了先有鸡还是先有蛋的问题。解决这个问题的方法就是协程创建者。launch, future, sequence都是协程创建者。顾名思义,协程创建者是用来创建协程对象的,除此之外和普通函数没有区别。它们就是通往协程世界和挂起函数的大门。在这个大门里,我们可以尽情地使用挂起函数,简化我们的计算过程。当然,这些都不是固定不变的,这些函数都有多个配置参数,其中最重要的就是CoroutineContext。
CoroutineContext
CoroutineContext的作用是提供协程的各种配置信息,本质上就是保存非重复元素的容器(Set),里面的元素可以根据Key获取到(如调度器),称之为元素(Element)。这里,我忍不住想把它的接口定义放出来,因为实在是太美了。
interface CoroutineContext {
operator fun <E : Element> get(key: Key<E>): E?
fun <R> fold(initial: R, operation: (R, Element) -> R): R
operator fun plus(context: CoroutineContext): CoroutineContext
fun minusKey(key: Key<*>): CoroutineContext
interface Element : CoroutineContext {
val key: Key<*>
}
interface Key<E : Element>
}
以上就是Kotlin中对CoroutineContext的定义,这些API每个都有其巧妙的用途,让人叹服
get目的是根据Key获取对应的对象,这个方法的奇特之处就是查询参数。利用这个方法在执行某个操作之前判断CoroutineContext是否有某个配置对象,从而实现一种权限认证。fold其实就是一种迭代算法,可以对全部元素进行检查。plus这就很有意思了,它可以让两个对象合并起来,并且当key相同时使用右侧的对象覆盖左侧的对象。这在我们的协程使用中绝对是最灵活的API了。我们可以使用+替换调原本的调度器,使用我们给定的调度器,而且看起来是那么自然。minusKey返回不包含指定key的context,相当于一种取反操作,这在某些情境下非常有用。
我觉得这应该算得上是对抽象的极致体现了,这个接口用简单的API抽象了增删改查四个操作,并且保留了强大的扩展性。在最开始接触协程的时候,我常常对协程复杂的工作机制和简单的参数配置产生了深深的怀疑,直到我看到了这个定义,我才明白它真正的强大之处,它不仅可以用系统默认的工作配置完成工作,还允许用户实现自己的CoroutineContext来随时替换掉默认配置,完成自己定制化的任务。
Continuation
Continuation不是Kotlin特有的概念,它在维基上的解释是一种控制状态的抽象表示。而在Kotlin中,它是对协程在挂起点的一个状态抽象,这可能不太好理解,我们可以通过具体的API来将这个概念具体化。
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)
}
它有个关键的函数——resumeWith,它表示在挂起状态之后的某个时刻,通过这个状态对象从原来的位置恢复过来。这是挂起函数实现的关键。而这里面的控制状态就是由参数体现了,成功或者失败,所以它还有两个扩展方法:
fun <T> Continuation<T>.resume(value: T)
fun <T> Continuation<T>.resumeWithException(exception: Throwable)
总结
协程是一个可被调度的计算实体,可通过协程创建者创建,在协程的代码块里可以使用挂起函数,它能必要的时候挂起,然后在条件满足后恢复,完成异步代码的串行化编程。
以上就是理解协程的关键概念,在实际使用协程的过程中可能用不到很多,但是却会对我们理解其运作过程很有帮助,也是写出标准协程代码的关键。Kotlin协程并没有很多黑魔法,只是为了适用多种不同的使用场景,有了庞大的API,本篇文章就是对这些API的一个概括解释,后面将会针对各种场景再进行详细梳理,希望大家喜欢。
青山不改,绿水长流,咱们下期见!
Kotlin协程-那些理不清乱不明的关系的更多相关文章
- Kotlin协程第一个示例剖析及Kotlin线程使用技巧
Kotlin协程第一个示例剖析: 上一次https://www.cnblogs.com/webor2006/p/11712521.html已经对Kotlin中的协程有了理论化的了解了,这次则用代码来直 ...
- Retrofit使用Kotlin协程发送请求
Retrofit2.6开始增加了对Kotlin协程的支持,可以通过suspend函数进行异步调用.本文简单介绍一下Retrofit中协程的使用 导入依赖 app的build文件中加入: impleme ...
- Kotlin协程基础
开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 我们已经通过第一个例子学会了启 ...
- Android Kotlin协程入门
Android官方推荐使用协程来处理异步问题.以下是协程的特点: 轻量:单个线程上可运行多个协程.协程支持挂起,不会使正在运行协程的线程阻塞.挂起比阻塞节省内存,且支持多个并行操作. 内存泄漏更少:使 ...
- Kotlin 协程一 —— 全面了解 Kotlin 协程
一.协程的一些前置知识 1.1 进程和线程 1.1.1基本定义 1.1.2为什么要有线程 1.1.3 进程与线程的区别 1.2 协作式与抢占式 1.2.1 协作式 1.2.2 抢占式 1.3 协程 二 ...
- rxjava回调地狱-kotlin协程来帮忙
本文探讨的是在tomcat服务端接口编程中, 异步servlet场景下( 参考我另外一个文章),用rxjava来改造接口为全流程异步方式 好处不用说 tomcat的worker线程利用率大幅提高,接口 ...
- Kotlin协程解析系列(上):协程调度与挂起
vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...
- Kotlin协程通信机制: Channel
Coroutines Channels Java中的多线程通信, 总会涉及到共享状态(shared mutable state)的读写, 有同步, 死锁等问题要处理. 协程中的Channel用于协程间 ...
- Kotlin协程作用域与Job详解
Job详解: 在上一次https://www.cnblogs.com/webor2006/p/11725866.html中抛出了一个问题: 所以咱们将delay去掉,需要改造一下,先把主线程的dela ...
- Kotlin协程作用域与构建器详解
在上次我们是通过了这种方式来创建了一个协程: 接着再来看另一种创建协程的方式: 下面用它来实现上一次程序一样的效果,先来回顾一下上一次程序的代码: 好,下面改用runBlocking的方式: 运行一下 ...
随机推荐
- 关于js类的继承
原型链继承 特点:基于原型链,既是父类的实例,也是子类的实例. 缺点: 无法实现多继承. 构造继承 特点: 可以实现多继承. 缺点: 之能继承父类实例的属性和方法,不能继承原型上的属性和方法. 实例继 ...
- VUE百度地图API调用(手机端、PC端、微信通用)
百度地图API-示例中心: https://lbsyun.baidu.com/jsdemo.htm#aCreateMap 1.引入百度地图(此处用到的是V2.0版本) 1> 建立一个js文件,例 ...
- Redis读书笔记(一)
Redis数据结构 1 简单动态字符串 Simple dynamic string 的实现 // sds.h/sdshdr struct sdshdr { int len; //记录buf数组中已使用 ...
- Laravel 代码开发最佳实践(持续更新)
我们这里要讨论的并不是 Laravel 版的 SOLID 原则(想要了解更多 SOLID 原则细节查看这篇文章)亦或是设计模式,而是 Laravel 实际开发中容易被忽略的最佳实践. 内容概览 单一职 ...
- 基于Containerd容器引擎和kubeadm工具部署K8sv1.26.3
前文我了解了基于ubuntu2204部署containerd容器引擎以及containerd客户端工具的部署和使用相关话题,回顾请参考:https://www.cnblogs.com/qiuhom-1 ...
- 【Java Se】JDBC
启停服务 net start mysql net stop mysql 登录 mysql -u -p 访问指定IP的mysql mysql -u root -P 3306 -h localhost - ...
- golang 必会之 pprof 监控系列(5) —— cpu 占用率 统计原理
golang pprof 监控系列(5) -- cpu 占用率 统计原理 大家好,我是蓝胖子. 经过前面的几节对pprof的介绍,对pprof统计的原理算是掌握了七八十了,我们对memory,bloc ...
- LeetCode 周赛 340,质数 / 前缀和 / 极大化最小值 / 最短路 / 平衡二叉树
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 上周跟大家讲到小彭文章风格的问题,和一些朋友聊过以后,至少在算法题解方面确定了小彭的风格 ...
- C 语言版线程池
一.初始线程池 1.1 何为线程池? 我们先来打个比方,线程池就好像一个工具箱,我们每次需要拧螺丝的时候都要从工具箱里面取出一个螺丝刀来.有时候需要取出一个来拧,有时候螺丝多的时候需要多个人取出多个来 ...
- 如何在现有项目中使用`Masa MiniApi`?
首先我们现有创建一个空的WebApi的项目模板,这个项目模板和MasaFramework本身没有任何关联,我们本博客只是使用的MasaFramework的MiniApi的包 创建Asp.NET Cor ...