Kotlin提供了协程来更加方便的实现异步代码。主要的部分就是suspend
方法以及配套的丰富的API和库。本文尽可能的用简单的语言来解释协程的基础概念。
什么是协程
Kotlin团队把它定义为“轻量级的线程”,它们是实际的线程可以执行的某种任务。
线程可以在某个“挂起点”停止执行协程,而去处理其他的任务。然后可以在之后继续执行这个协程,也有可能是在另外一个线程上执行。
所以,一个协程不是一个任务,而是一系列的“子任务”。这些“子任务”会按照特定的顺序执行。即使代码看起来是在一个顺序的代码块里的,协程里对“挂起函数”的调用时间也是顺序执行的。这就需要我们一探挂起函数的究竟了。
挂起函数
在kotlinx的delay
或者是Ktor的HttpClient.post
函数的定义都带有关键字suspend
。
suspend fun delay(timeMillis: Long) {...}
suspend fun someNetworkCallReturningValue(): SomeType {
...
}
这些函数就叫做挂起函数。挂起函数可以挂起当前协程的执行而不会阻塞所在的线程。
也就是说挂起函数可能在某个点停止了执行,而在之后的某个时间点又继续执行。然而这里没有说到当前线程会干什么。
挂起函数都是顺序的
挂起函数并没有什么特别的返回类型。除了多了一个suspend
关键字并没有其他特别的地方。也不需要类似于Java的Future
或者JavaScript的Promise
之类的包装器。这也就更加确定了挂起函数本身并不是异步的(至少从调用者的角度看是这样),也不像JavaScript的async
方法需要返回一个promise。
在挂起函数里调用其他的挂起函数和平常的函数调用没什么区别:被调用的函数执行完之后才会继续执行剩下的代码。
suspend fun someNetworkCallReturningSomething(): Something {
// some networking operations making use of the suspending mechanism
}
suspend fun someBusyFunction(): Unit {
delay(1000L)
println("Printed after 1 second")
val something: Something = someNetworkCallReturningSomething()
println("Received $something from network")
}
如此一来复杂的异步代码写起来也就相当容易了。
挂起和非挂起怎么连接到一起
直接在非挂起函数里调用挂起函数是无法编译的。这是因为只有协程里才可以调用挂起函数,所以我们要新建一个协程先。这就需要用到协程构造器:
协程构造器
协程构造器就是新建了一个挂起函数,然后调用其他的挂起函数。他们可以在非挂起函数内被调用,是因为他们本身不是挂起函数,也就可以扮演一个普通函数和挂起函数的桥梁。
Kotlin提供了很多种不同的协程构造器,我们来认识几种:
阻塞当前线程的runBlocking
这是最简单的协程构造器了。它会阻塞当前线程一直等到里面的挂起函数都执行完毕:
fun main() {
println("Hello,")
// we create a coroutine running the provided suspending lambda
// and block the main thread while waiting for the coroutine to finish its execution
runBlocking {
// now we are inside a coroutine
delay(2000L) // suspends the current coroutine for 2 seconds
}
// will be executed after 2 seconds
println("World!")
}
可以看到runBlocking
的定义,需要传入的最后一个参数是一个挂起函数,但是它本身不是(阻塞线程):
fun <T> runBlocking(
...,
block: suspend CoroutineScope.() -> T
): T {
...
}
runBlocking
经常用在讲解协程时候的hello world例子里阻塞main方法显示挂起函数执行的结果。
“launch”发射后不用管
一般协程是不阻塞所在的线程的,而是开始一个异步任务。协程构造器launch
就是用来在后台开始一个异步任务的。比如:
fun main() {
GlobalScope.launch { // launch new coroutine in background and continue
delay(1000L)
println("World!")
}
println("Hello,") // main thread continues here immediately
runBlocking { // but this expression blocks the main thread
delay(2000L) // ... while we delay for 2 seconds to keep JVM alive
}
}
这个会打印出“Hello”, 随后打印出“World”。
GlobalScope
不用急,后面会详细的讲到。
本例中为了可以看到输出的结果所以在最后还是阻塞的了线程。
使用“async”获得异步任务的结果
这是另外一个协程构造器async
。这个构造器可以得到异步任务执行的返回值。
fun main() {
val deferredResult: Deferred<String> = GlobalScope.async {
delay(1000L)
"World!"
}
runBlocking {
println("Hello, ${deferredResult.await()}")
}
}
async
构造器会返回一个Deferred
类型的对象,这和Future
或者Promise
类似。之后通过await
调用可以得到异步任务的返回结果。
await
不是一个简单的阻塞方法,它是一个挂起函数。也就是说这个不能直接在mian方法里调用await
,所以上例中是在runBlocking
里调用的。
这里再次出现了GlobalScope
。协程的scope是用来创建结构化的并发的。
结构化并发
从上面的例子里,你会发现他们有个共同点:阻塞并等待协程执行完成。
kotlin可以席间结构化的协程,这样父协程可以管理子协程的生命周期。他可以等待所有的子协程完成,或者一个子协程发生异常的时候取消所有的子协程。
创建结构化的协程
除了runBlocking
,一般不在协程里调用,所有的协程构造器都定义在CoroutineScope
的扩展里面:
fun <T> runBlocking(...): T {...}
fun <T> CoroutineScope.async(...): Deferred<T> {...}
fun <T> CoroutineScope.launch(...): Job {...}
fun <E> CoroutineScope.produce(...): ReceiveChannel<E> {...}
...
要新建一个协程,你要么用GlobalScope
(新建一个顶层协程),要么用一个已经存在的协程scope的扩展方法。有一个可以说是某种约定,最好是写一个CoroutineScope
的扩展方法来新建协程。
async
的定义如下:
fun <T> CoroutineScope.async(
...
block: suspend CoroutineScope.() -> T
): Deferred<T> {
...
}
看上面的代码,async传入的参数也是一个CoroutineScope
的扩展。也就是说你可以在里面调用协程构造器而不用指定调用对象。
上面的例子的可以这样修改:
fun main() = runBlocking {
val deferredResult = async {
delay(1000L)
"World!"
}
println("Hello, ${deferredResult.await()}")
}
fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
fun main() = runBlocking {
delay(1000L)
println("Hello, World!")
}
我们不再需要GlobalScope
了,因为runBlocking
已经提供了一个scope了。
coroutineScope构造器
上面说过runBlocking
不鼓励使用。因为Kotlin的协程的初衷就是不阻塞线程。不过runBlocking
就是一个coroutineScope
构造器。
coroutineScope
会挂起了当前的协程,一直到所有的子协程都执行完毕。例如:
fun main() = runBlocking { // this: CoroutineScope
launch {
delay(200L)
println("Task from runBlocking")
}
coroutineScope { // Creates a new coroutine scope
launch {
delay(500L)
println("Task from nested launch")
}
delay(100L)
println("Task from coroutine scope") // This line will be printed before nested launch
}
println("Coroutine scope is over") // This line is not printed until nested launch completes
}