初遇 Kotlin 协程(coroutine)
这篇文章我们将建立协程项目, 并用 Coroutines 编写相关代码.
Kotlin 1.1 引入了协程程序, 这是一种编写异步, 非阻塞代码 (以及其他) 的新方法. 在这篇文章中, 我们将使用 kotlinx.coroutines 库来了解基本的协程写法, 这个库是对已存的 JAVA 库的封装.
Setting up a project
我们将使用 Gradle 来构建项目.
加入库依赖:
- dependencies {
- ...
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1"
- }
这个库托管在 JCenter 仓库中, 我们加入仓库地址:
- repositories {
- jcenter()
- }
Coroutine 第一行代码
我们可以认为协程是一种轻量级的线程. 像线程一样, 协程可以并行, 可以相互间通信. 和线程最大的不同是, 协程是非常轻量级的, 我们可以创建几千个协程, 但是消耗的性能却非常非常的少. 而对于真的线程, 这是非常耗资源的. 几千个线程对于现代计算机来说是一个严重的挑战.
我们怎么启动一个协程呢? 可以使用 launch{}函数:
launch{ ... }
完整的例子如下:
- println("Start")
- GlobalScope.launch {
- delay(1000)
- println("Hello")
- }
- Thread.sleep(2000) // wait for 2 seconds
- println("Stop")
这里我们启动了一个协程, 等待一秒后, 打印了 Hello.
我们使用了 delay() 这个方法, 这个方法类似 Thread.sleep() , 但是更好, 它不阻塞线程, 只是挂起协程本身. 当协程挂起等待是, 返回到线程; 当协程等待完成时, 协程恢复继续运行.
在这个例子中, 主线程 main() 必须等待协程完成, 否则在输出 Hello 之前, 程序就结束了.
假如我们在 main() 中直接使用 delay() 函数, 将会遇到编译错误:
Suspend functions are only allowed to be called from a coroutine or another suspend function
这是因为挂起函数只能运行在协程中, 我们可以使用 runBlocking() 来启动一个协程.
- runBlocking {
- delay(2000)
- }
复杂一点的例子
让我们来看下协程是不是轻量级的, 我们启动一百万个线程:
- val c = AtomicLong()
- for (i in 1..1_000_000L)
- thread(start = true) {
- c.addAndGet(i)
- }
- println(c.get())
这个例子将会运行较长一段时间, 消耗较多的资源.:(
我们换种方式, 用协程来实现:
- val c = AtomicLong()
- for (i in 1..1_000_000L)
- GlobalScope.launch {
- c.addAndGet(i)
- }
- println(c.get())
这个例子几秒就完成了, 对比线程, 有着显著的优势.
Async: 从协程中返回值
另一种启动协程的方法是使用 async{} , 它类似 launch{}, 但是它返回 Deferred<T> 实例, 这个实例有 await() 方法, 该方法返回协程结果.
我们启动一百万个协程, 然后持有它们的返回结果 Deferred.
- val deferred = (1..1_000_000).map { n ->
- GlobalScope.async {
- n
- }
- }
这些协程都已经启动, 我们把它们加起来.
val sum = deferred.sumBy { it.await() }
我们使用了标准函数库 sumBy, 来把他们加在一起, 但是我们简单这样做, 编译器会报错:
Suspend functions are only allowed to be called from a coroutine or another suspend function
因为 await() 是挂起函数, 不能用在协程外面, 正如上面说过的一样. 我们把它放在协程里:
- runBlocking {
- val sum = deferred.sumBy { it.await() }
- println("Sum: $sum")
- }
现在它将会顺利输出结果. 我们稍微改下代码, 确认这个百万个协程是平行的, 假如我们再每个启动的协程里延时一秒, 看看是否要花费百万秒才会输出结果:
- val deferred = (1..1_000_000).map { n ->
- GlobalScope.async {
- delay(1000)
- n
- }
- }
我们可以运行下, 就可以知道结果. 结果是只用了十几秒. 显然, 它是并行的.
Suspending functions
现在我们把里边的代码提取出来:
- fun workload(n: Int): Int {
- delay(1000)
- return n
- }
一个类似的错误将会出现:
Suspend functions are only allowed to be called from a coroutine or another suspend function
让我们进一步看看这个是什么意思. 协程最大的优势是可以不阻塞线程的挂起. 编译器将会组织特殊的代码来达到这个可能, 所以我们使用 suspend 来修饰一个方法:
- suspend fun workload(n: Int): Int {
- delay(1000)
- return n
- }
现在我们可以在协程种调用 workload 方法, 编译器知道这个方法可能会挂起, 并做相应的工作.
- GlobalScope.async {
- workload(n)
- }
我们的 workload 方法能够在协程中被调用, 或者别的挂起函数, 但是不能够在协程外调用. 相应的, delay() 和 await() 函数被 suspend 修饰, 这就是为什么它们只能在挂起函数中被调用, 或者在协程内被调用, runBlocking(), launch{}, 或者 async().
来源: http://www.jianshu.com/p/b7e91d6d5678