-
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
我们看到在协程被暂停的那一刻,协程外面的代码被执行了。一段时间之后,协程被继续执行,打印结果。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
总结
现在新技术层出不穷,如果每次出新的技术,我们都深入的研究的话,很容易分散精力。新的技术可能很久之后我们才会在工作中用得上,当学的新技术无法学以致用,很容易被我们遗忘,到最后真的需要使用的时候,又要从头来过(虽然上手会更快)。
我觉得身为技术人,针对新技术应该是持拥抱态度的,入了这一行你就应该知道这是一个活到老学到老的行业,所以面对新技术,不要抵触,拥抱变化就好了。
Flutter 明显是一种全新的技术,而对于这个新技术在发布之初,花一个月的时间学习它,成本确实过高。但是周末花一天时间体验一下它的开发流程,了解一下它的优缺点、能干什么或者不能干什么。这个时间,并不是我们不能接受的。
如果有时间,其实通读一遍 Flutter 的文档,是最全面的一次对 Flutter 的了解过程。但是如果我们只有 8 小时的时间,我希望能关注一些最值得关注的点。
(跨平台开发(Flutter)、java基础与原理,自定义view、NDK、架构设计、性能优化、完整商业项目开发等)
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
学习它,成本确实过高。但是周末花一天时间体验一下它的开发流程,了解一下它的优缺点、能干什么或者不能干什么。这个时间,并不是我们不能接受的。
如果有时间,其实通读一遍 Flutter 的文档,是最全面的一次对 Flutter 的了解过程。但是如果我们只有 8 小时的时间,我希望能关注一些最值得关注的点。
(跨平台开发(Flutter)、java基础与原理,自定义view、NDK、架构设计、性能优化、完整商业项目开发等)
[外链图片转存中…(img-rLZ9IDrH-1712521351544)]
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算