初遇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
实例,这个实例有 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()
。