Kotlin Coroutines(协程) 完全解析(一),协程
本文基于 Kotlin v1.2.31,Kotlin-Coroutines v0.22.5
Kotlin 中引入 Coroutine(协程) 的概念,可以帮助编写异步代码,目前还是试验性的。国内详细介绍协程的资料比较少,所以我打算写 Kotlin Coroutines(协程) 完全解析的系列文章,希望可以帮助大家更好地理解协程。这是系列文章的第一篇,简单介绍协程的特点和一些基本概念。协程主要的目的是简化异步编程,那么先从为什么需要协程来编写异步代码开始。
1. 为什么需要协程?
异步编程中最为常见的场景是:在后台线程执行一个复杂任务,下一个任务依赖于上一个任务的执行结果,所以必须等待上一个任务执行完成后才能开始执行。看下面代码中的三个函数,后两个函数都依赖于前一个函数的执行结果。
fun requestToken(): Token {
// makes request for a token & waits
return token // returns result when received
}
fun createPost(token: Token, item: Item): Post {
// sends item to the server & waits
return post // returns resulting post
}
fun processPost(post: Post) {
// does some local processing of result
}
三个函数中的操作都是耗时操作,因此不能直接在 UI 线程中运行,而且后两个函数都依赖于前一个函数的执行结果,三个任务不能并行运行,该如何解决这个问题呢?
1.1 回调
常见的做法是使用回调,把之后需要执行的任务封装为回调。
fun requestTokenAsync(cb: (Token) -> Unit) { ... }
fun createPostAsync(token: Token, item: Item, cb: (Post) -> Unit) { ... }
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
requestTokenAsync { token ->
createPostAsync(token, item) { post ->
processPost(post)
}
}
}
回调在只有两个任务的场景是非常简单实用的,很多网络请求框架的 onSuccess Listener 就是使用回调,但是在三个以上任务的场景中就会出现多层回调嵌套的问题,而且不方便处理异常。
1.2 Future
Java 8 引入的 CompletableFuture 可以将多个任务串联起来,可以避免多层嵌套的问题。
fun requestTokenAsync(): CompletableFuture<Token> { ... }
fun createPostAsync(token: Token, item: Item): CompletableFuture<Post> { ... }
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
requestTokenAsync()
.thenCompose { token -> createPostAsync(token, item) }
.thenAccept { post -> processPost(post) }
.exceptionally { e ->
e.printStackTrace()
null
}
}
上面代码中使用连接符串联起三个任务,最后的exceptionally
方法还可以统一处理异常情况,但是只能在 Java 8 以上才能使用。
1.3 Rx 编程
CompletableFuture 的方式有点类似 Rx 系列的链式调用,这也是目前大多数推荐的做法。
fun requestToken(): Token { ... }
fun createPost(token: Token, item: Item): Post { ... }
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
Single.fromCallable { requestToken() }
.map { token -> createPost(token, item) }
.subscribe(
{ post -> processPost(post) }, // onSuccess
{ e -> e.printStackTrace() } // onError
)
}
RxJava 丰富的操作符、简便的线程调度、异常处理使得大多数数满意,我也如此,但是还没有更简洁易读的写法呢?
1.4 协程
下面是使用 Kotlin 协程的代码:
suspend fun requestToken(): Token { ... } // 挂起函数
suspend fun createPost(token: Token, item: Item): Post { ... } // 挂起函数
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
launch {
val token = requestToken()
val post = createPost(token, item)
processPost(post)
// 需要异常处理,直接加上 try/catch 语句即可
}
}
使用协程后的代码非常简洁,以顺序的方式书写异步代码,不会阻塞当前 UI 线程,错误处理也和平常代码一样简单。
2. 协程是什么
2.1 Gradle 引入
dependencies {
// Kotlin
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
// Kotlin Coroutines
compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.5'
}
kotlin {
experimental {
coroutines 'enable' // avoid compiler reports a warning every time when we use the experimental coroutines
}
}
2.2 协程的定义
先看官方文档的描述:
协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。
协程的开发人员 Roman Elizarov 是这样描述协程的:协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。
总而言之:协程可以简化异步编程,可以顺序地表达程序,协程也提供了一种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的方法 -- 协程挂起。
3. 协程的基本概念
下面通过上面协程的例子来介绍协程中的一些基本概念:
3.1 挂起函数
suspend fun requestToken(): Token { ... } // 挂起函数
suspend fun createPost(token: Token, item: Item): Post { ... } // 挂起函数
fun processPost(post: Post) { ... }
requestToken
和createPost
函数前面有suspend
修饰符标记,这表示两个函数都是挂起函数。挂起函数能够以与普通函数相同的方式获取参数和返回值,但是调用函数可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起),挂起函数挂起协程时,不会阻塞协程所在的线程。挂起函数执行完成后会恢复协程,后面的代码才会继续执行。但是挂起函数只能在协程中或其他挂起函数中调用。事实上,要启动协程,至少要有一个挂起函数,它通常是一个挂起 lambda 表达式。所以suspend
修饰符可以标记普通函数、扩展函数和 lambda 表达式。
挂起函数只能在协程中或其他挂起函数中调用,上面例子中launch
函数就创建了一个协程。
fun postItem(item: Item) {
launch { // 创建一个新协程
val token = requestToken()
val post = createPost(token, item)
processPost(post)
// 需要异常处理,直接加上 try/catch 语句即可
}
}
launch
函数:
public actual fun launch(
context: CoroutineContext = DefaultDispatcher,
start: CoroutineStart = CoroutineStart.DEFAULT,
parent: Job? = null,
block: suspend CoroutineScope.() -> Unit
): Job
从上面函数定义中可以看到协程的一些重要的概念:CoroutineContext、CoroutineDispatcher、Job,下面来一一介绍这些概念。
3.1 CoroutineContext
CoroutineContext,协程上下文,是一些元素的集合,主要包括 Job 和 CoroutineDispatcher 元素,可以代表一个协程的场景。
EmptyCoroutineContext 表示一个空的协程上下文。
3.2 CoroutineDispatcher
CoroutineDispatcher,协程调度器,决定协程所在的线程或线程池。它可以指定协程运行于特定的一个线程、一个线程池或者不指定任何线程(这样协程就会运行于当前线程)。coroutines-core
中 CoroutineDispatcher 有两种标准实现 CommonPool 和 Unconfined,Unconfined 就是不指定线程。
launch
函数定义中的DefaultDispatcher
实际上就是CommonPool
,CommonPool
是一个协程调度器,其指定的线程为共有的线程池。而且 CoroutineDispatcher 实现了 CoroutineContext 接口,所以才能直接指定context: CoroutineContext = DefaultDispatcher
,实际上,协程上下文中的元素都实现了 CoroutineContext 接口。
3.3 Job & Deferred
Job,任务,封装了协程中需要执行的代码逻辑。Job 可以取消并且有简单生命周期,它有三种状态:
State | [isActive] | [isCompleted] | [isCancelled] |
---|---|---|---|
New (optional initial state) | false |
false |
false |
Active (default initial state) | true |
false |
false |
Completing (optional transient state) | true |
false |
false |
Cancelling (optional transient state) | false |
false |
true |
Cancelled (final state) | false |
true |
true |
Completed (final state) | false |
true |
false |
Job 完成时是没有返回值的,如果需要返回值的话,应该使用 Deferred,它是 Job 的子类public actual interface Deferred<out T> : Job
。Deferred 额外有一种状态 isCompletedExceptionally:
State | [isActive] | [isCompleted] | [isCompletedExceptionally] | [isCancelled] |
---|---|---|---|---|
New (optional initial state) | false |
false |
false |
false |
Active (default initial state) | true |
false |
false |
false |
Completing (optional transient state) | true |
false |
false |
false |
Cancelling (optional transient state) | false |
false |
false |
true |
Cancelled (final state) | false |
true |
true |
true |
Resolved (final state) | false |
true |
false |
false |
Failed (final state) | false |
true |
true |
false |
3.4 Coroutine builders
launch
函数属于协程构建器 Coroutine builders,Kotlin 中还有其他几种 Builders,负责创建协程。
3.4.1 launch {}
launch {}
是最常用的 Coroutine builders,不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器,例如在 Android 中常用的launch(UI) {}
。
fun postItem(item: Item) {
launch(UI) { // 在 UI 线程创建一个新协程
val token = requestToken()
val post = createPost(token, item)
processPost(post)
}
}
3.4.2 runBlocking {}
runBlocking {}
是创建一个新的协程同时阻塞当前线程,直到协程结束。这个不应该在协程中使用,主要是为main
函数和测试设计的。
fun main(args: Array<String>) = runBlocking<Unit> { // start main coroutine
launch { // launch new coroutine in background and continue
delay(1000L)
println("World!")
}
println("Hello,") // main coroutine continues here immediately
delay(2000L) // delaying for 2 seconds to keep JVM alive
}
class MyTest {
@Test
fun testMySuspendingFunction() = runBlocking<Unit> {
// here we can use suspending functions using any assertion style that we like
}
}
3.4.3 withContext {}
withContext {}
不会创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成。
3.4.4 async {}
async {}
可以实现与 launch builder 一样的效果,在后台创建一个新协程,唯一的区别是它有返回值,因为async {}
返回的是 Deferred 类型。
fun main(args: Array<String>) = runBlocking<Unit> { // start main coroutine
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() } // start async one coroutine without suspend main coroutine
val two = async { doSomethingUsefulTwo() } // start async two coroutine without suspend main coroutine
println("The answer is ${one.await() + two.await()}") // suspend main coroutine for waiting two async coroutines to finish
}
println("Completed in $time ms")
}
获取async {}
的返回值需要通过await()
函数,它也是是个挂起函数,调用时会挂起当前协程直到 async 中代码执行完并返回某个值。
4. 小结
Kotlin 协程可以极大地简化异步编程,期待它正式发布的一天。虽然刚开始接触的时候学习比较吃力,但是接触过一段时间相信绝对会爱上它。建议大家也去浏览下面推荐的资料,可以更快地了解协程的大概。
推荐阅读:
-
Introduction to Coroutines(Roman Elizarov at KotlinConf 2017, slides)