深入理解 Kotlin coroutine (二)

这个例子是 kotlinx.coroutines 的第一个小例子。

fun main(args: Array<String>) { launch(CommonPool) { // create new coroutine in common thread pool delay(1000L) // non-blocking delay for 1 second (default time unit is ms) println("World!") // print after delay } println("Hello,") // main function continues while coroutine is delayed Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

这个例子的运行结果是:

Hello, World!

  • 1

  • 2

其实有了上一篇文章的基础我们很容易知道,launch 方法启动了一个协程,CommonPool 是一个有线程池的上下文,它可以负责把协程的执行分配到合适的线程上。所以从线程的角度来看,打印的这两句是在不同的线程上的。

20170206-063015.015 [main] Hello, 20170206-063016.016 [ForkJoinPool.commonPool-worker-1] World!

  • 1

  • 2

这段代码的执行效果与线程的版本看上去是一样的:

thread(name = "MyThread") { Thread.sleep(1000L) log("World!") } log("Hello,") Thread.sleep(2000L)

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

3. 主线程上的协程


我们刚才通过 launch 创建的协程是在 CommonPool 的线程池上面的,所以协程的运行并不在主线程。如果我们希望直接在主线程上面创建协程,那怎么办?

fun main(args: Array<String>) = runBlocking<Unit> { launch(CommonPool) { delay(1000L) println("World!") } println("Hello,") delay(2000L) }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

这个还是 kotlinx.coroutines 的例子,我们来分析一下。runBlocking 实际上也跟 launch 一样,启动一个协程,只不过它传入的 context 不会进行线程切换,也就是说,由它创建的协程会直接运行在当前线程上。

在 runBlocking 当中通过 launch 再创建一个协程,显然,这段代码的运行结果与上一个例子是完全一样的。需要注意的是,尽管我们可以在协程中通过 launch 这样的方法创建协程,但不要再协程当中通过 runBlocking 再来创建协程,因为这样做虽然一般来说不会导致程序异常,不过,这样的程序也没有多大意义:

fun main(args: Array<String>) = runBlocking<Unit> { runBlocking { delay(1000L) println("World!") } println("Hello,") }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

运行结果:

World! Hello,

  • 1

  • 2

大家看到了,嵌套的 runBlocking 实际上仍然只是一段顺序代码而已。

那么,让我们再仔细看看前面的例子,不知道大家有没有问题:如果我在 launch 创建的协程当中多磨叽一会儿,主线程上的协程 delay(2000L) 好像也没多大用啊。有没有什么方法保证协程执行完?

4. 外部控制协程


我们在上一篇文章当中只是对内置的基础 API 进行了简单的封装,而 kotlinx.coroutines 却为我们做了非常多的事情。比如,每一个协程都看做一个 Job,我们在一个协程的外部也可以控制它的运行。

fun main(args: Array<String>) = runBlocking<Unit> { val job = launch(CommonPool) { delay(1000L) println("World!") } println("Hello,") job.join() }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

job.join 其实就是要求当前协程等待 job 执行完成之后再继续执行。

其实,我们还可以取消协程,让他直接停止执行:

fun main(args: Array<String>) = runBlocking<Unit> { val job = launch(CommonPool) { delay(1000L) println("World!") } println("Hello,") job.cancel() }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

job.cancel 会直接终止 job 的执行。如果 job 已经执行完毕,那么 job.cancel 的执行时没有意义的。我们也可以根据 cancel 的返回值来判断是否取消成功。

另外,cancel 还可以提供原因:

job.cancel(IllegalAccessException("World!"))

  • 1

如果我们提供了这个原因,那么被取消的协程会将它打印出来。

Hello, Exception in thread "main" java.lang.IllegalAccessException: World! at example13.Example_13Kt$main$1.doResume(example-13.kt:14) at kotlin.coroutines.experimental.jvm.internal.CoroutineImpl.resume(CoroutineImpl.kt:53) at kotlinx.coroutines.experimental.DispatchedContinuation$resume$1.run(CoroutineDispatcher.kt:57)

  • 1

  • 2

  • 3

  • 4

  • 5

其实,如果你自己做过对线程任务的取消,你大概会知道除非被取消的线程自己去检查取消的标志位,或者被 interrupt,否则取消是无法实现的,这有点儿像一个人执意要做一件事儿,另一个人说你别做啦,结果人家压根儿没听见,你说他能停下来吗?那么我们前面的取消到底是谁去监听了这个 cancel 操作呢?

当然是 delay 这个操作了。其实所有 kotlinx.coroutines 当中定义的操作都可以做到这一点,我们对代码稍加改动,你就会发现异常来自何处了:

val job = launch(CommonPool) { try { delay(1000L) println("World!") } catch(e: Exception) { e.printStackTrace() }finally { println("finally....") } } println("Hello,") job.cancel(IllegalAccessException("World!"))

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

是的,你没看错,我们居然可以在协程里面对 cancel 进行捕获,如果你愿意的话,你甚至可以继续在这个协程里面运行代码,但请不要这样做,下面的示例破坏了 cancel 的设计本意,所以请勿模仿:

val job = launch(CommonPool) { try { ... }finally { println("finally....") } println("I'm an EVIL!!! Hahahaha") }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

说这个是什么意思呢?在协程被 cancel 掉的时候,我们应该做的其实是把战场打扫干净,比如:

val job = launch(CommonPool) { val inputStream = ... try { ... }finally { inputStream.close() } }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

我们再来考虑下面的情形:

fun main(args: Array<String>) = runBlocking<Unit> { val job = launch(CommonPool) { var nextPrintTime = 0L var i = 0 while (true) { // computation loop val currentTime = System.currentTimeMillis() if (currentTime >= nextPrintTime) { println("I'm sleeping ${i++} ...") nextPrintTime = currentTime + 500L } } } delay(1300L) // delay a bit println("main: I'm tired of waiting!") job.cancel() // cancels the job delay(1300L) // delay a bit to see if it was cancelled.... println("main: Now I can quit.") }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

不得不说,kotlinx.coroutines 在几天前刚刚更新的文档和示例非常的棒。我们看到这个例子,while(true) 会让这个协程不断运行来模拟耗时计算,尽管外部调用了 job.cancel(),但由于内部并没有 care 自己是否被 cancel,所以这个 cancel 显然有点儿失败。如果你想要在类似这种耗时计算当中检测当前协程是否被取消的话,你可以这么写:

... while (isActive) { // computation loop ... } ...

  • 1

  • 2

  • 3

  • 4

  • 5

isActive 会在 cancel 之后被置为 false。

其实,通过这几个示例大家就会发现协程的取消,与我们通常取消线程操作的思路非常类似,只不过人家封装的比较好,而我们呢,每次还得自己搞一个 CancelableTask 来实现 Runnable 接口去承载自己的异步操作,想想也是够原始呢。

5. 轻量级线程


协程时轻量级的,它拥有自己的运行状态,但它对资源的消耗却非常的小。其实能做到这一点的本质原因,我们已经在上一篇文章当中提到过,一台服务器开 1k 线程和 1k 协程来响应服务,前者对资源的消耗必然很大,而后者可能只是基于很少的几个或几十个线程来工作的,随着请求数量的增加,协程的优势可能会体现的更加明显。

我们来看个比较简单的例子:

fun main(args: Array<String>) = runBlocking<Unit> { val jobs = List(100_000) { launch(CommonPool) { delay(1000L) print(".") } } jobs.forEach { it.join() } //这里不能用 jobs.forEach(Job::join),因为 Job.join 是 suspend 方法 }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

通过 List 这个方法,我们可以瞬间创建出很多对象放入返回的 List,注意到这里的 jobs 其实就是协程的一个 List。

运行上面的代码,我们发现 CommonPool 当中的线程池的线程数量基本上维持在三四个就足够了,如果我们用线程来写上面的代码会是什么感觉?

fun main(args: Array<String>) = runBlocking<Unit> { val jobs = List(100_000) { thread { Thread.sleep(1000L) log(".") } } jobs.forEach(Thread::join) // Thread::join 说起来也是 1.1 的新特性呢! }

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

运行时,在创建了 1k 多个线程之后,就抛出了异常:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method)

  • 1

  • 2

嗯,又多了一个用协程的理由,对不对?

6. 携带值的 Job


我们前面说了,通过携程返回的 Job,我们可以控制携程的运行。可有时候我们更关注协程运行的结果,比如从网络加载一张图片:

suspend fun loadImage(url: String): Bitmap { ... return ... }

  • 1

  • 2

  • 3

  • 4

没错,我们更关注它的结果,这种情况我们该怎么办呢?如果 loadImage 不是 suspend 方法,那么我们在非 UI 线程当中直接获取他们:

val imageA = loadImage(urlA) val imageB = loadImage(urlB) onImageGet(imageA, imageB)

总结

最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上相关的我搜集整理的Android开发中高级必知必会核心笔记,共计2968页PDF、58w字,囊括Android开发648个知识点,我把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包知识脉络 + 诸多细节。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

2021年虽然路途坎坷,都在说Android要没落,但是,不要慌,做自己的计划,学自己的习,竞争无处不在,每个行业都是如此。相信自己,没有做不到的,只有想不到的。

虽然面试失败了,但我也不会放弃入职字节跳动的决心的!建议大家面试之前都要有充分的准备,顺顺利利的拿到自己心仪的offer。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
map { … return … }`

  • 1

  • 2

  • 3

  • 4

没错,我们更关注它的结果,这种情况我们该怎么办呢?如果 loadImage 不是 suspend 方法,那么我们在非 UI 线程当中直接获取他们:

val imageA = loadImage(urlA) val imageB = loadImage(urlB) onImageGet(imageA, imageB)

总结

最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上相关的我搜集整理的Android开发中高级必知必会核心笔记,共计2968页PDF、58w字,囊括Android开发648个知识点,我把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包知识脉络 + 诸多细节。

[外链图片转存中…(img-CfSdUo8v-1715337198889)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

2021年虽然路途坎坷,都在说Android要没落,但是,不要慌,做自己的计划,学自己的习,竞争无处不在,每个行业都是如此。相信自己,没有做不到的,只有想不到的。

虽然面试失败了,但我也不会放弃入职字节跳动的决心的!建议大家面试之前都要有充分的准备,顺顺利利的拿到自己心仪的offer。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值