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()
}
}

同样的逻辑,将readwrite实现为挂起函数后,能写成什么样呢?

 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返回不包含指定keycontext,相当于一种取反操作,这在某些情境下非常有用。

我觉得这应该算得上是对抽象的极致体现了,这个接口用简单的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协程-那些理不清乱不明的关系的更多相关文章

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

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

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

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

  3. Kotlin协程基础

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

  4. Android Kotlin协程入门

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

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

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

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

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

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

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

  8. Kotlin协程通信机制: Channel

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

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

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

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

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

随机推荐

  1. 温故知新----线程之Runnable与Callable接口的本质区别

    温故知新----线程之Runnable与Callable接口的本质区别 预备知识:Java中的线程对象是Thread,新建线程也只有通过创建Thread对象的实例来创建. 先说结论 1 Runnabl ...

  2. instanceof 的原理

    涉及面试题: instanceof 的原理是什么? instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是 能找到类型的 prototype 实现一下 instan ...

  3. R数据分析:生存分析的列线图的理解与绘制详细教程

    列线图作为一个非常简单明了的临床辅助决策工具,在临床中用的(发文章的)还是比较多的,尤其是肿瘤预后: Nomograms are widely used for cancer prognosis, p ...

  4. 非线性规划—R实现

    非线性规划 非线性规划是一种求解目标函数或约束条件中有一个或几个非线性函数的最优化问题的方法.运筹学八大分支之一,20世纪50年代初,库哈(H.W.Kuhn) 和托克 (A.W.Tucker) 提出了 ...

  5. 在Kubernetes(k8s)中使用GPU

    介绍 Kubernetes 支持对节点上的 AMD 和 NVIDIA GPU (图形处理单元)进行管理,目前处于实验状态. 修改docker配置文件 root@hello:~# cat /etc/do ...

  6. Unity学习笔记02 —— C#语法

    C#语法 控制台 Console Console.WriteLine(); Console.ReadLine(); 随机数 Random Random random = new Random(); r ...

  7. idea快捷键--增强for循环

    增强for循环,用于遍历:数组或单列集合 快捷键: 数组.for

  8. Go语言:两种常见的并发模型

    Go语言:两种常见的并发模型 在并发编程中,须要精确地控制对共享资源的访问,Go语言将共享的值通过通道传递 并发版"Hello World" 使用goroutine来打印" ...

  9. 记一次python写爬虫爬取学校官网的文章

    有一位老师想要把官网上有关数字化的文章全部下载下来,于是找到我,使用python来达到目的 首先先查看了文章的网址 获取了网页的源代码发现一个问题,源代码里面没有url,这里的话就需要用到抓包了,因为 ...

  10. Qt 加载 libjpeg 库出现“长跳转已经运行”错误

    继上篇 Qt5.15.0 升级至 Qt5.15.9 遇到的一些错误 篇幅有点长,先说解决方法,在编译静态库时加上 -qt-libjpeg,编译出 libjpeg 库后,在项目中使用 #pragma c ...