深入理解 Kotlin Coroutine (一)(1)

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

运行结果如下:

co-body 1 10 foo 2 main true 4 co-body r main true 11 -9 co-body x y main true 10 end main false cannot resume dead coroutine

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

首先定义了一个 foo 函数,然后创建 coroutine,创建了之后还需要调用 resume 才能执行协程,运行过程是谦让的,是交替的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图中数字表示第n次

协程为我们的程序提供了一种暂停的能力,就好像状态机,只有等到下一次输入,它才做状态转移。显然,用协程来描述一个状态机是再合适不过的了。

也许大家对 lua 的语法不是很熟悉,不过没关系,上面的例子只需要知道大概是在干什么就行:这例子就好像,main 和 Foo 在交替干活,有点儿像 A B 两个人分工协作,A 干一会儿 B 来,B 干一会儿,再让 A 来一样。如果我们用线程来描述这个问题,那么可能会用到很多回调,相信写 Js 的兄弟听到这儿要感到崩溃了,因为 Js 的代码写着写着就容易回调满天飞,业务逻辑的实现越来越抽象,可读性越来越差;而用协程的话,就好像一个很平常的同步操作一样,一点儿异步任务的感觉都没有。

我们前面提到的协程的非抢占调度方式,以及这个交替执行代码的例子,基本上可以说明协程实际上致力于用同步一样的代码来完成异步任务的运行。

一句话,有了协程,你的异步程序看起来就像同步代码一样。

2. Kotlin 协程初体验


Kotlin 1.1 对协程的基本支持都在 Kotlin 标准库当中,主要涉及两个类和几个包级函数和扩展方法:

  • CoroutineContext,协程的上下文,这个上下文可以是多个的组合,组合的上下文可以通过 key 来获取。EmptyCoroutineContext 是一个空实现,没有任何功能,如果我们在使用协程时不需要上下文,那么我们就用这个对象作为一个占位即可。上下文这个东西,不管大家做什么应用,总是能遇到,比如 Android 里面的 Context,JSP 里面的 PageContext 等等,他们扮演的角色都大同小异:资源管理,数据持有等等,协程的上下文也基本上是如此。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • Continuation,顾名思义,继续、持续的意思。我们前面说过,协程提供了一种暂停的能力,可继续执行才是最终的目的,Continuation 有两个方法,一个是 resume,如果我们的程序没有任何异常,那么直接调用这个方法并传入需要返回的值;另一个是 resumeWithException,如果我们的程序出了异常,那我们可以通过调用这个方法把异常传递出去。

  • 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 协程的基本操作,包括创建、启动、暂停和继续,继续的操作在 Continuation 当中,剩下的三个都是包级函数或扩展方法:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这几个类和函数其实与我们前面提到的 Lua 的协程 API 非常相似,都是协程最基础的 API。

除此之外,Kotlin 还增加了一个关键字:suspend,用作修饰会被暂停的函数,被标记为 suspend 的函数只能运行在协程或者其他 suspend 函数当中。

好,介绍完这些基本概念,让我们来看一个例子:

fun main(args: Array<String>) { log("before coroutine") //启动我们的协程 asyncCalcMd5("test.zip") { log("in coroutine. Before suspend.") //暂停我们的线程,并开始执行一段耗时操作 val result: String = suspendCoroutine { continuation -> log("in suspend block.") continuation.resume(calcMd5(continuation.context[FilePath]!!.path)) log("after resume.") } log("in coroutine. After suspend. result = $result") } log("after coroutine") }   /** * 上下文,用来存放我们需要的信息,可以灵活的自定义 */ class FilePath(val path: String): AbstractCoroutineContextElement(FilePath){ companion object Key : CoroutineContext.Key<FilePath> }   fun asyncCalcMd5(path: String, block: suspend () -> Unit) { val continuation = object : Continuation<Unit> { override val context: CoroutineContext get() = FilePath(path)   override fun resume(value: Unit) { log("resume: $value") }   override fun resumeWithException(exception: Throwable) { log(exception.toString()) } } block.startCoroutine(continuation) }   fun calcMd5(path: String): String{ log("calc md5 for $path.") //暂时用这个模拟耗时 Thread.sleep(1000) //假设这就是我们计算得到的 MD5 值 return System.currentTimeMillis().toString() }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

  • 31

  • 32

  • 33

  • 34

  • 35

  • 36

  • 37

  • 38

  • 39

  • 40

  • 41

  • 42

  • 43

  • 44

  • 45

  • 46

  • 47

这段程序在模拟计算文件的 Md5 值。我们知道,文件的 Md5 值计算是一项耗时操作,所以我们希望启动一个协程来处理这个耗时任务,并在任务运行结束时打印出来计算的结果。

我们先来一段一段分析下这个示例:

/** * 上下文,用来存放我们需要的信息,可以灵活的自定义 */ class FilePath(val path: String): AbstractCoroutineContextElement(FilePath){ companion object Key : CoroutineContext.Key<FilePath> }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

我们在计算过程中需要知道计算哪个文件的 Md5,所以我们需要通过上下文把这个路径传入协程当中。如果有多个数据,也可以一并添加进去,在运行当中,我们可以通过 Continuation 的实例拿到上下文,进而获取到这个路径:

continuation.context[FilePath]!!.path

  • 1

接着,我们再来看下 Continuation:

val continuation = object : Continuation<Unit> { override val context: CoroutineContext get() = FilePath(path)   override fun resume(value: Unit) { log("resume: $value") }   override fun resumeWithException(exception: Throwable) { log(exception.toString()) } }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

我们除了给定了 FilePath 这样一个上下文之外就是简单的打了几行日志,比较简单。这里传入的 Continuation 当中的 resume 和 resumeWithException 只有在协程最终执行完成后才会被调用,这一点需要注意一下,也正是因为如此,startCoroutine 把它叫做 completion:

public fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>

  • 1

那么下面我们看下最关键的这段代码:

asyncCalcMd5("test.zip") { log("in coroutine. Before suspend.") //暂停我们的协程,并开始执行一段耗时操作 val result: String = suspendCoroutine { continuation -> log("in suspend block.") continuation.resume(calcMd5(continuation.context[FilePath]!!.path)) log("after resume.") } log("in coroutine. After suspend. result = $result") }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

suspendCoroutine 这个方法将外部的代码执行权拿走,并转入传入的 Lambda 表达式中,而这个表达式当中的操作就对应异步的耗时操作了,在这里我们“计算”出了 Md5 值,接着调用 continuation.resume 将结果传了出去,传给了谁呢?传给了 suspendCoroutine 的返回值也即 result,这时候协程继续执行,打印 result 结束。

下面就是运行结果了:

2017-01-30T06:43:52.284Z [main] before coroutine 2017-01-30T06:43:52.422Z [main] in coroutine. Before suspend. 2017-01-30T06:43:52.423Z [main] in suspend block. 2017-01-30T06:43:52.423Z [main] calc md5 for test.zip. 2017-01-30T06:43:53.426Z [main] after resume. 2017-01-30T06:43:53.427Z [main] in coroutine. After suspend. result = 1485758633426 2017-01-30T06:43:53.427Z [main] resume: 1485758633426 2017-01-30T06:43:53.427Z [main] after coroutine

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

细心的读者肯定一看就发现,所谓的异步操作是怎么个异步法?从日志上面看,明明上面这段代码就是顺序执行的嘛,不然 after coroutine 这句日志为什么非要等到最后才打印?

还有,整个程序都只运行在了主线程上,我们的日志足以说明这一点了,根本没有异步嘛。难道说协程就是一个大骗子??

3. 实现异步


这一部分我们就要回答上一节留下的问题。不过在此之前,我们再来回顾一下协程存在的意义:让异步代码看上去像同步代码,直接自然易懂。至于它如何做到这一点,可能各家的语言实现各有不同,但协程给人的感觉更像是底层并发 API(比如线程)的语法糖。当然,如果你愿意,我们通常所谓的线程也可以被称作操作系统级 API 的语法糖了吧,毕竟各家语言对于线程的实现也各有不同,这个就不是我们今天要讨论的内容了。

不管怎么样,你只需要知道,协程的异步需要依赖比它更底层的 API 支持,那么在 Kotlin 当中,这个所谓的底层 API 就非线程莫属了。

知道了这一点,我们就要考虑想办法来把前面的示例完善一下了。

首先我们实例化一个线程池:

private val executor = Executors.newSingleThreadScheduledExecutor { Thread(it, "scheduler") }

  • 1

  • 2

  • 3

接着我们把计算 Md5 的部分交给线程池去运行:

asyncCalcMd5("test.zip") { log("in coroutine. Before suspend.") //暂停我们的线程,并开始执行一段耗时操作 val result: String = suspendCoroutine { continuation -> log("in suspend block.") executor.submit { continuation.resume(calcMd5(continuation.context[FilePath]!!.path)) log("after resume.") } } log("in coroutine. After suspend. result = $result") executor.shutdown() }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

那么结果呢?

2017-01-30T07:18:04.496Z [main] before coroutine 2017-01-30T07:18:04.754Z [main] in coroutine. Before suspend. 2017-01-30T07:18:04.757Z [main] in suspend block. 2017-01-30T07:18:04.765Z [main] after coroutine 2017-01-30T07:18:04.765Z [scheduler] calc md5 for test.zip. 2017-01-30T07:18:05.769Z [scheduler] in coroutine. After suspend. result = 1485760685768 2017-01-30T07:18:05.769Z [scheduler] resume: 1485760685768 2017-01-30T07:18:05.769Z [scheduler] after resume.

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

我们看到在协程被暂停的那一刻,协程外面的代码被执行了。一段时间之后,协程被继续执行,打印结果。

截止到现在,我们用协程来实现异步操作的功能已经实现。

你可能要问,如果我们想要完成异步操作,直接用线程池加回调岂不更直接简单,为什么要用协程呢,搞得代码这么让人费解不说,也没有变的很简单啊。

说的对,如果我们实际当中把协程的代码都写成这样,肯定会被蛋疼死,我前面展示给大家的,是 Kotlin 标准库当中最为基础的 API,看起来非常的原始也是理所应当的,如果我们对其加以封装,那效果肯定大不一样。

除此之外,在高并发的场景下,多个协程可以共享一个或者多个线程,性能可能会要好一些。举个简单的例子,一台服务器有 1k 用户与之连接,如果我们采用类似于 Tomcat 的实现方式,一个用户开一个线程去处理请求,那么我们将要开 1k 个线程,这算是个不小的数目了;而我们如果使用协程,为每一个用户创建一个协程,考虑到同一时刻并不是所有用户都需要数据传输,因此我们并不需要同时处理所有用户的请求,那么这时候可能只需要几个专门的 IO 线程和少数来承载用户请求对应的协程的线程,只有当用户有数据传输事件到来的时候才去相应,其他时间直接挂起,这种事件驱动的服务器显然对资源的消耗要小得多。

4. 进一步封装


这一节的内容较多的参考了 Kotlin 官方的

4.1 异步

刚才那个示例让我们感觉到,写个协程调用异步代码实在太原始了,所以我们决定对它做一下封装。如果我们能在调用 suspendCoroutine 的时候直接把后面的代码拦截,并切到线程池当中执行,那么我们就不用每次自己搞一个线程池来做这事儿了,嗯,让我们研究下有什么办法可以做到这一点。

拦截…怎么拦截呢?

public interface ContinuationInterceptor : CoroutineContext.Element { companion object Key : CoroutineContext.Key<ContinuationInterceptor>   public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> }

  • 1

  • 2

  • 3

  • 4

  • 5

我们发现,Kotlin 的协程 API 当中提供了这么一个拦截器,可以把协程的操作拦截,传入的是原始的 Continuation,返回的是我们经过线程切换的 Continuation,这样就可以实现我们的目的了。

open class Pool(val pool: ForkJoinPool) : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {   override fun <T> interceptContinuation(continuation: Continuation<T>) : Continuation<T> = PoolContinuation(pool, //下面这段代码是要查找其他拦截器,并保证能调用它们的拦截方法 continuation.context.fold(continuation, { cont, element -> if (element != this@Pool && element is ContinuationInterceptor) element.interceptContinuation(cont) else cont })) }   private class PoolContinuation<T>( val pool: ForkJoinPool, val continuation: Continuation<T> ) : Continuation<T> by continuation { override fun resume(value: T) { if (isPoolThread()) continuation.resume(value) else pool.execute { continuation.resume(value) } }   override fun resumeWithException(exception: Throwable) { if (isPoolThread()) continuation.resumeWithException(exception) else pool.execute { continuation.resumeWithException(exception) } }   fun isPoolThread(): Boolean = (Thread.currentThread() as? ForkJoinWorkerThread)?.pool == pool }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

这个 Pool 是什么鬼?我们让它继承 AbstractCoroutineContextElement 表明它其实就是我们需要的上下文。实际上这个上下文可以给任意协程使用,于是我们再定义一个 object:

object CommonPool : Pool(ForkJoinPool.commonPool())

  • 1

有了这个,我们就可以把没加线程池的版本改改了:

fun main(args: Array<String>) { log("before coroutine") //启动我们的协程 asyncCalcMd5("test.zip") { ... } log("after coroutine") //加这句的原因是防止程序在协程运行完之前停止 CommonPool.pool.awaitTermination(10000, TimeUnit.MILLISECONDS) }   ...   fun asyncCalcMd5(path: String, block: suspend () -> String) { val continuation = object : Continuation<String> { override val context: CoroutineContext //注意这个写法,上下文可以通过 + 来组合使用 get() = FilePath(path) + CommonPool   ... } block.startCoroutine(continuation) }   ...

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

那么运行结果呢?

2017-01-30T09:13:11.183Z [main] before coroutine 2017-01-30T09:13:11.334Z [main] after coroutine 2017-01-30T09:13:11.335Z [ForkJoinPool.commonPool-worker-1] in coroutine. Before suspend. 2017-01-30T09:13:11.337Z [ForkJoinPool.commonPool-worker-1] in suspend block. 2017-01-30T09:13:11.337Z [ForkJoinPool.commonPool-worker-1] calc md5 for test.zip. 2017-01-30T09:13:12.340Z [ForkJoinPool.commonPool-worker-1] after resume. 2017-01-30T09:13:12.341Z [ForkJoinPool.commonPool-worker-1] in coroutine. After suspend. result = 1485767592340 2017-01-30T09:13:12.341Z [ForkJoinPool.commonPool-worker-1] resume: 1485767592340

  • 1

  • 2

  • 3
    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

结语

由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!以下是目录截图:

由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。

再附一部分Android架构面试视频讲解:

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-gFqXwF2G-1712015817321)]

[外链图片转存中…(img-8GVev6I4-1712015817322)]

[外链图片转存中…(img-jf9oCSnl-1712015817322)]

[外链图片转存中…(img-7nLxlMUe-1712015817322)]

[外链图片转存中…(img-NvYhgnpY-1712015817322)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

结语

由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!以下是目录截图:

[外链图片转存中…(img-Kh1EY5yg-1712015817322)]

由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。

再附一部分Android架构面试视频讲解:

[外链图片转存中…(img-R04ZIMII-1712015817323)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值