本文基于 Kotlin v1.2.31,Kotlin-Coroutines v0.22.5
Kotlin 中引入 Coroutine(协程) 的概念,可以帮助编写异步代码,目前还是试验性的。国内详细介绍协程的资料比较少,所以我打算写 Kotlin Coroutines(协程) 完全解析的系列文章,希望可以帮助大家更好地理解协程。这是系列文章的第一篇,简单介绍协程的特点和一些基本概念。协程主要的目的是简化异步编程,那么先从为什么需要协程来编写异步代码开始。
异步编程中最为常见的场景是:在后台线程执行一个复杂任务,下一个任务依赖于上一个任务的执行结果,所以必须等待上一个任务执行完成后才能开始执行。看下面代码中的三个函数,后两个函数都依赖于前一个函数的执行结果。
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 线程中运行,而且后两个函数都依赖于前一个函数的执行结果,三个任务不能并行运行,该如何解决这个问题呢?
常见的做法是使用回调,把之后需要执行的任务封装为回调。
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 就是使用回调,但是在三个以上任务的场景中就会出现多层回调嵌套的问题,而且不方便处理异常。
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 以上才能使用。
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 丰富的操作符、简便的线程调度、异常处理使得大多数数满意,我也如此,但是还没有更简洁易读的写法呢?
下面是使用 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 线程,错误处理也和平常代码一样简单。
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 } }
先看官方文档的描述:
协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。
协程的开发人员 Roman Elizarov 是这样描述协程的: 协程就像非常轻量级的线程 。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以 协程也像用户态的线程 ,非常轻量级,一个线程中可以创建任意个协程。
总而言之:协程可以简化异步编程,可以顺序地表达程序,协程也提供了一种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的方法 – 协程挂起。
下面通过上面协程的例子来介绍协程中的一些基本概念:
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,下面来一一介绍这些概念。
CoroutineContext,协程上下文,是一些元素的集合,主要包括 Job 和 CoroutineDispatcher 元素,可以代表一个协程的场景。
EmptyCoroutineContext 表示一个空的协程上下文。
CoroutineDispatcher,协程调度器,决定协程所在的线程或线程池。它可以指定协程运行于特定的一个线程、一个线程池或者不指定任何线程(这样协程就会运行于当前线程)。 coroutines-core
中 CoroutineDispatcher 有两种标准实现 CommonPool 和 Unconfined,Unconfined 就是不指定线程。
launch
函数定义中的 DefaultDispatcher
实际上就是 CommonPool
, CommonPool
是一个协程调度器,其指定的线程为共有的线程池。而且 CoroutineDispatcher 实现了 CoroutineContext 接口,所以才能直接指定 context: CoroutineContext = DefaultDispatcher
,实际上,协程上下文中的元素都实现了 CoroutineContext 接口。
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
|
launch
函数属于协程构建器 Coroutine builders,Kotlin 中还有其他几种 Builders,负责创建协程。
launch {}
是最常用的 Coroutine builders,不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器,例如在 Android 中常用的 launch(UI) {}
。
fun postItem(item:Item) { launch(UI) { // 在 UI 线程创建一个新协程 val token = requestToken() val post = createPost(token, item) processPost(post) } }
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 } }
withContext {}
不会创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成。
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$timems") }
获取 async {}
的返回值需要通过 await()
函数,它也是是个挂起函数,调用时会挂起当前协程直到 async 中代码执行完并返回某个值。