深入理解 Kotlin coroutine (二)
原文链接:https://github.com/enbandari/Kotlin-Tutorials
上周我们把 Kotlin Coroutine 的基本 API 挨个讲了一下,也给出了一些简单的封装。
真是不要太给脸,就在前几天发布的 1.1 Beta 2 当中,所有协程的 API 包名后面都加了一个 experimental,这意味着 Kotlin 官方在 1.1 当中还是倾向于将 Coroutine 作为一个实验性质的特性的,不过,这也没关系,我们学习的心不以外界的变化而变化不是?
这一篇我们基于前面的基础来了解一下 Kotlinx.coroutines 这个库的使用,如果大家对它的实现原理有兴趣,可以再读一读上一篇文章,我们也可以在后面继续写一些文章来给深入地大家介绍。
1. 准备工作
就像前面我们说到的,1.1 Beta 2 当中协程相关的基础库的包名都增加了 experimental,所以我们在选择 kotlinx.coroutines 的版本的时候也一定要对应好编译器的版本,不然…你自己想哈哈。
我们强调一下,kotlin 的版本选择 1.1.0-beta-38,kotlinx.coroutines 的版本选择 0.6-beta,如果你恰好使用 gradle,那么告诉你一个好消息,我会直接告诉你怎么配置:
buildscript { ext.kotlin_version = '1.1.0-beta-38' repositories { jcenter() maven { url "http://dl.bintray.com/kotlin/kotlin-eap-1.1" } } ...
} repositories { jcenter() maven { url "http://dl.bintray.com/kotlin/kotlin-eap-1.1" }
} kotlin { experimental { coroutines 'enable' }
} dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.6-beta'
}
2. 一个基本的协程的例子
这个例子是 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
}
这个例子的运行结果是:
Hello,
World!
其实有了上一篇文章的基础我们很容易知道,launch 方法启动了一个协程,CommonPool 是一个有线程池的上下文,它可以负责把协程的执行分配到合适的线程上。所以从线程的角度来看,打印的这两句是在不同的线程上的。
20170206-063015.015 [main] Hello,
20170206-063016.016 [ForkJoinPool.commonPool-worker-1] World!
这段代码的执行效果与线程的版本看上去是一样的:
thread(name = "MyThread") { Thread.sleep(1000L) log("World!")
}
log("Hello,")
Thread.sleep(2000L)
3. 主线程上的协程
我们刚才通过 launch 创建的协程是在 CommonPool 的线程池上面的,所以协程的运行并不在主线程。如果我们希望直接在主线程上面创建协程,那怎么办?
fun main(args: Array<String>) = runBlocking<Unit> { launch(CommonPool) { delay(1000L) println("World!") } println("Hello,") delay(2000L)
}
这个还是 kotlinx.coroutines 的例子,我们来分析一下。runBlocking 实际上也跟 launch 一样,启动一个协程,只不过它传入的 context 不会进行线程切换,也就是说,由它创建的协程会直接运行在当前线程上。
在 runBlocking 当中通过 launch 再创建一个协程,显然,这段代码的运行结果与上一个例子是完全一样的。需要注意的是,尽管我们可以在协程中通过 launch 这样的方法创建协程,但不要再协程当中通过 runBlocking 再来创建协程,因为这样做虽然一般来说不会导致程序异常,不过,这样的程序也没有多大意义:
fun main(args: Array<String>) = runBlocking<Unit> { runBlocking { delay(1000L) println("World!") } println("Hello,")
}
运行结果:
World!
Hello,
大家看到了,嵌套的 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()
}
job.join 其实就是要求当前协程等待 job 执行完成之后再继续执行。
其实,我们还可以取消协程,让他直接停止执行:
fun main(args: Array<String>) = runBlocking<Unit> { val job = launch(CommonPool) { delay(1000L) println("World!") } println("Hello,") job.cancel()
}
job.cancel 会直接终止 job 的执行。如果 job 已经执行完毕,那么 job.cancel 的执行时没有意义的。我们也可以根据 cancel 的返回值来判断是否取消成功。
另外,cancel 还可以提供原因:
job.cancel(IllegalAccessException("World!"))
如果我们提供了这个原因,那么被取消的协程会将它打印出来。
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)
其实,如果你自己做过对线程任务的取消,你大概会知道除非被取消的线程自己去检查取消的标志位,或者被 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!"))
是的,你没看错,我们居然可以在协程里面对 cancel 进行捕获,如果你愿意的话,你甚至可以继续在这个协程里面运行代码,但请不要这样做,下面的示例破坏了 cancel 的设计本意,所以请勿模仿:
val job = launch(CommonPool) { try { ... }finally { println("finally....") } println("I'm an EVIL!!! Hahahaha")
}
说这个是什么意思呢?在协程被 cancel 掉的时候,我们应该做的其实是把战场打扫干净,比如:
val job = launch(CommonPool) { val inputStream = ...try {...}finally {inputStream.close()}
}
我们再来考虑下面的情形:
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.")
}
不得不说,kotlinx.coroutines 在几天前刚刚更新的文档和示例非常的棒。我们看到这个例子,while(true) 会让这个协程不断运行来模拟耗时计算,尽管外部调用了 job.cancel(),但由于内部并没有 care 自己是否被 cancel,所以这个 cancel 显然有点儿失败。如果你想要在类似这种耗时计算当中检测当前协程是否被取消的话,你可以这么写:
...
while (isActive) { // computation loop ...
}
...
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 方法
}
通过 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 的新特性呢!
}
运行时,在创建了 1k 多个线程之后,就抛出了异常:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method)
嗯,又多了一个用协程的理由,对不对?
6. 携带值的 Job
我们前面说了,通过携程返回的 Job,我们可以控制携程的运行。可有时候我们更关注协程运行的结果,比如从网络加载一张图片:
suspend fun loadImage(url: String): Bitmap { ... return ...
}
没错,我们更关注它的结果,这种情况我们该怎么办呢?如果 loadImage 不是 suspend 方法,那么我们在非 UI 线程当中直接获取他们:
val imageA = loadImage(urlA)
val imageB = loadImage(urlB)
onImageGet(imageA, imageB)
这样的操作有什么问题?顺序获取两张图片,耗时,不经济。所以传统的做法就是开两个线程做这件事儿,这意味着你会看到两个回调,并且还要同步这两个回调,想想都头疼。
不过我们现在有更好的办法:
val imageA = defer(CommonPool) { loadImage(urlA) }
val imageB = defer(CommonPool) { loadImage(urlB) }
onImageGet(imageA.await(),imageB.await())
代码量几乎没有增加,不过我们却做到了两张图片异步获取,并同时传给 onImageGet 以便继续后面的操作。
defer 到底是个什么东西?其实大家大可不必看到新词就感到恐慌,这东西用法几乎跟 launch 一样,只不过它返回的 Deferred 功能比 Job 多了一样:携带返回值。我们前面看到的 imageA 其实就是一个 Deferred 实例,而它的 await 方法返回的则是 Bitmap 类型,也即 loadImage(urlA) 的返回值。
所以如果你对协程运行的结果感兴趣,直接使用 defer 来替换你的 launch 就可以了。需要注意的是,即便你不调用 await,defer 启动的协程也会立即运行,如果你希望你的协程能够按需启动,例如只有你调用 await 之后再启动,那么你可以用 lazyDefer:
val imageA = lazyDefer(CommonPool) { loadImage(urlA) }
val imageB = lazyDefer(CommonPool) { loadImage(urlB) }
onImageGet(imageA.await(),imageB.await()) //这时候才开始真正去加载图片
7. 生成器
不知道大家对 python 的生成器有没有了解,这个感觉就好似延迟计算一样。
假设我们要计算 fibonacci 数列,这个大家都知道,也非常容易写,你可能分分钟写出一个递归的函数来求得这个序列,不过你应该知道递归的层级越多,stackOverflow 的可能性越大吧?另外,如果我们只是用到其中的几个,那么递归的函数一下子都给求出来,而且每次调用也没有记忆性导致同一个值计算多次,非常不经济。大家看一个 python 的例子:
def fibonacci(): yield 1 # 直接返回 1, 并且在此处暂停 first = 1 second = 1 while True: yield first first, second = first + second, first a = fibonacci()
for x in a: print x if x > 100: break
前面给出的这种计算方法,fibonacci 函数返回一个可迭代的对象,这个对象其实就是生成器,只有我们在迭代它的时候,它才会去真正执行计算,只要遇到 yield,那么这一次迭代到的值就是 yield 后面的值,比如,我们第一次调用 fibonacci 这个函数的时候,得到的值就是 1,后面依次类推。
Kotlin 在添加了协程这个功能之后,也可以这么搞了:
val fibonacci = buildSequence { yield(1) // first Fibonacci number var cur = 1 var next = 1 while (true) { yield(next) // next Fibonacci number val tmp = cur + next cur = next next = tmp }
}
...
for (i in fibonacci){ println(i) if(i > 100) break //大于100就停止循环
}
可以这么说,这段代码与前面的 python 版本功能是完全相同的,在 yield 方法调用时,传入的值就是本次迭代的值。
fibonacci 这个变量的类型如下:
public interface Sequence<out T> { public operator fun iterator(): Iterator<T>
}
既然有 iterator 方法,那么我们可以直接对 fibonacci 进行迭代也就没什么大惊小怪的了。这个 iterator 保证每次迭代的时候去执行 buildSequence 后面的 Lambda 的代码,从上一个 yield 之后开始到下一个 yield 结束,yield 传入的值就是 iterator 的 next 的返回值。
有了这个特性,我们就可以构造许多“懒”序列,只有在用到的时候才去真正计算每一个元素的值,而且运算状态可以保存,每次计算的结果都不会浪费。
注:这个特性是被 Kotlin 标准库收录了的,并不存在于 kotlinx.coroutines 当中,不过这也没关系啦,kotlinx.coroutines 的 API 会不会在不久的将来也作为 Kotlin 标准库的内容出现呢?
8. 小结
这一篇的内容其实相对上一篇要简单一些,面对 kotlinx.coroutines 这样的框架,我们直接通过分析案例,将 coroutine 这么理论化的东西投入实际场景,让大家从感性上对其有个更加深入的认识。
当然,我们并没有深入其中了解其原理,原因就是上一篇我们为此做了足够的准备 —— kotlinx.coroutines 作为官方的框架,自然要实现得完善一些,但也是万变不离其宗。
写到这里,我想,我们还是需要有一篇文章再来介绍一些协程使用的一些注意事项,那么我们下一篇再见吧。
深入理解 Kotlin coroutine (二)相关推荐
- 深入理解 Kotlin Coroutine (一)
原文链接:https://github.com/enbandari/Kotlin-Tutorials 本文主要介绍 Kotlin Coroutine 的基础 API,有关 Kotlinx.Corout ...
- pdf 深入理解kotlin协程_Kotlin协程实现原理:挂起与恢复
今天我们来聊聊Kotlin的协程Coroutine. 如果你还没有接触过协程,推荐你先阅读这篇入门级文章What? 你还不知道Kotlin Coroutine? 如果你已经接触过协程,但对协程的原理存 ...
- 深入理解C/C++二维数组
深入理解C/C++二维数组 前言 本来以为自己对二维数组的理解还可以,没感觉有什么,但是今天小伙伴问了一个问题感觉迷惑了好久,于是决定细致的记录一下,一步一步的探究各种关于二维数组的问题,巩固基础. ...
- TLD(Tracking-Learning-Detection)学习与源码理解之(二)
TLD(Tracking-Learning-Detection)学习与源码理解之(二) zouxy09@qq.com http://blog.csdn.net/zouxy09 OpenTLD下载与编译 ...
- 深入理解常见的二十三种设计模式
深入理解常见的二十三种设计模式 文章目录 深入理解常见的二十三种设计模式 一.设计模式的分类 1.1 创建型(五种) 1.2 结构型(七种) 1.3 行为型(十一种) 二.创建型 2.1 单例模式 2 ...
- 深入理解Plasma(二)Plasma 细节
最新内容会更新在主站深入浅出区块链社区 原文链接:深入理解Plasma(二)Plasma 细节 这一系列文章将围绕以太坊的二层扩容框架,介绍其基本运行原理,具体操作细节,安全性讨论以及未来研究方向等. ...
- Scratch3.0——助力新进程序员理解程序(十二、画笔)
Scratch3.0--助力新进程序员理解程序(十二.画笔) 目录 Scratch3.0--助力新进程序员理解程序(十二.画笔) 前言 环境 下载地址 安装说明 1.菜单栏 2.功能栏 3.代码区 4 ...
- 看《墨攻》理解IoC概念(二 )http://www.qqread.com/erp/3/j380307_3.html
看<墨攻>理解IoC概念(二) 巧巧电脑网络 2007-11-07 IT168 陈雄华 收藏此文 大 中 小 分享到QQ空间人人网开心网豆瓣新浪微博腾讯微博更多0 上一页123下一 ...
- Kotlin Coroutine
原文链接 今天我们来聊聊Kotlin Coroutine,如果你还没有了解过,那么我要提前恭喜你,因为你将掌握一个新技能,对你的代码方面的提升将是很好的助力. What Coroutine ? 简单的 ...
最新文章
- Unity Application Block-配置文件的使用-示例(附×××)
- @Autowired、@Resource
- 运行javac编译报错:仅当显式请求注释处理时才接受类名称“xxxxxx”
- Educational Codeforces Round 52: D. Three Pieces(记忆化搜索)
- HDU - 4565 So Easy!
- 一定要讲给孩子们的27个小故事
- web测试-WEB常用测试用例集
- php 悬浮按钮,Android_Android利用悬浮按钮实现翻页效果,今天给大家分享下自己用悬浮 - phpStudy...
- JPBC库实现基于身份的签名体制
- 以下11條小建議,幫助你們的異地戀一直保持活力
- Docker拉取Solace pubsub+镜像timeout的问题
- wiondows如何更新pip版本
- 钟汉良日记:你知道吗?写日记可以影响三代人!
- 全球与中国激光光导鼓市场深度分析及发展研究预测报告
- java设计九宫格拼图软件哪个好用_九宫格拼图软件下载_抖音很火的九宫格拼图软件app下载_易玩网...
- Linux X 视窗编程基础
- 挑战最强大脑——来自全球的14个编码社区
- 【Kotlin 协程】Flow 异步流 ② ( 使用 Flow 异步流持续获取不同返回值 | Flow 异步流获取返回值方式与其它方式对比 | 在 Android 中使用 Flow 异步流下载文件 )
- 【从零开始游戏开发】Unity3D AssetBundle资源加载和封装 | 全面总结 | 建议收藏
- 对象存储和块存储的区别