深入理解 Kotlin Coroutine (一)

  • 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

  • 4

  • 5

  • 6

  • 7

  • 8

我们看到程序已经非常完美的实现异步调用。显然,这种写法要比线程池回调的写法看上去顺理成章得多。

4.2 启动协程

在讨论完异步的封装后,有人肯定还是会提出新问题:启动协程的写法是不是有点儿啰嗦了啊?没错,每次构造一个 Continuation,也没干多少事儿,实在没什么必要,干脆封装一个通用的版本得了:

class StandaloneCoroutine(override val context: CoroutineContext): Continuation<Unit> { override fun resume(value: Unit) {}   override fun resumeWithException(exception: Throwable) { //处理异常 val currentThread = Thread.currentThread() currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception) } }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

这样就好办了,我们每次启动协程只需要针对当前协程提供特定的上下文即可,那么我们是不是再把启动的那个函数改改呢?

fun launch(context: CoroutineContext, block: suspend () -> Unit) = block.startCoroutine(StandaloneCoroutine(context))

  • 1

  • 2

有了这个,我们前面的代码就可以进一步修改:

fun main(args: Array<String>) { log("before coroutine") //启动我们的协程 launch(FilePath("test.zip") + CommonPool) { 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") CommonPool.pool.awaitTermination(10000, TimeUnit.MILLISECONDS) }   /** * 上下文,用来存放我们需要的信息,可以灵活的自定义 */ class FilePath(val path: String) : AbstractCoroutineContextElement(Key) { companion object Key : CoroutineContext.Key<FilePath> } 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

运行结果自然也没什么好说的。

4.3 暂停协程

暂停协程这块儿也太乱了,看着莫名其妙的,能不能直白一点儿呢?其实我们的代码不过是想要获取 Md5 的值,所以如果能写成下面这样就好了:

val result = calcMd5(continuation.context[FilePath]!!.path).await()

  • 1

毋庸置疑,这肯定是可以的。想一下,有哪个类可以支持我们直接阻塞线程,等到获取到结果之后再返回呢?当然是 Future 了。

suspend fun <T> CompletableFuture<T>.await(): T { return suspendCoroutine { continuation -> whenComplete { result, e -> if (e == null) continuation.resume(result) else continuation.resumeWithException(e) } } }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

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

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

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

img

img

img

img

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

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

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

最后我还整理了很多Android中高级的PDF技术文档。以及一些大厂面试真题解析文档。

image

Android高级架构师之路很漫长,一起共勉吧!

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

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

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

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

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

[外链图片转存中…(img-8uIUzSCP-1712468913811)]

[外链图片转存中…(img-t87ioxOY-1712468913812)]

[外链图片转存中…(img-vwoGGq9B-1712468913813)]

[外链图片转存中…(img-vs89ev2G-1712468913813)]

[外链图片转存中…(img-W5Y87hFM-1712468913814)]

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

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

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

最后我还整理了很多Android中高级的PDF技术文档。以及一些大厂面试真题解析文档。

[外链图片转存中…(img-hUhjkaLi-1712468913814)]

Android高级架构师之路很漫长,一起共勉吧!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值