Kotlin Coroutines(协程) 完全解析(一),协程简介

Kotlin Coroutines(协程) 完全解析系列:

Kotlin Coroutines(协程) 完全解析(一),协程简介

Kotlin Coroutines(协程) 完全解析(二),深入理解协程的挂起、恢复与调度

Kotlin Coroutines(协程) 完全解析(三),封装异步回调、协程间关系及协程的取消

Kotlin Coroutines(协程) 完全解析(四),协程的异常处理

Kotlin Coroutines(协程) 完全解析(五),协程的并发

本文基于 Kotlin v1.3.0-rc-146,Kotlin-Coroutines v1.0.0-RC1

Kotlin 中引入 Coroutine(协程) 的概念,可以帮助编写异步代码,目前还是试验性的。国内详细介绍协程的资料比较少,所以我打算写 Kotlin Coroutines(协程) 完全解析的系列文章,希望可以帮助大家更好地理解协程。这是系列文章的第一篇,简单介绍协程的特点和一些基本概念。协程主要的目的是简化异步编程,那么先从为什么需要协程来编写异步代码开始。

Kotlin Coroutine 终于正式发布了,所以我跟进最新的正式版更新了本文相关内容

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 { ... }
fun createPostAsync(token: Token, item: Item): CompletableFuture { ... }
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) {
    GlobalScope.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:1.0.0-RC1'
}

2.2 协程的定义

先看官方文档的描述:

协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

协程的开发人员 Roman Elizarov 是这样描述协程的:协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。

总而言之:协程可以简化异步编程,可以顺序地表达程序,协程也提供了一种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的方法 -- 协程挂起。

3. 协程的基本概念

下面通过上面协程的例子来介绍协程中的一些基本概念:

3.1 挂起函数

suspend fun requestToken(): Token { ... }   // 挂起函数
suspend fun createPost(token: Token, item: Item): Post { ... }  // 挂起函数
fun processPost(post: Post) { ... }

requestTokencreatePost函数前面有suspend修饰符标记,这表示两个函数都是挂起函数。挂起函数能够以与普通函数相同的方式获取参数和返回值,但是调用函数可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起),挂起函数挂起协程时,不会阻塞协程所在的线程。挂起函数执行完成后会恢复协程,后面的代码才会继续执行。但是挂起函数只能在协程中或其他挂起函数中调用。事实上,要启动协程,至少要有一个挂起函数,它通常是一个挂起 lambda 表达式。所以suspend修饰符可以标记普通函数、扩展函数和 lambda 表达式。

挂起函数只能在协程中或其他挂起函数中调用,上面例子中launch函数就创建了一个协程。

fun postItem(item: Item) {
    GlobalScope.launch { // 创建一个新协程
        val token = requestToken()
        val post = createPost(token, item)
        processPost(post)
        // 需要异常处理,直接加上 try/catch 语句即可
    }
}

launch函数:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

从上面函数定义中可以看到协程的一些重要的概念:CoroutineContext、CoroutineDispatcher、Job,下面来一一介绍这些概念。

3.1 CoroutineScope 和 CoroutineContext

CoroutineScope,可以理解为协程本身,包含了 CoroutineContext。

CoroutineContext,协程上下文,是一些元素的集合,主要包括 Job 和 CoroutineDispatcher 元素,可以代表一个协程的场景。

EmptyCoroutineContext 表示一个空的协程上下文。

3.2 CoroutineDispatcher

CoroutineDispatcher,协程调度器,决定协程所在的线程或线程池。它可以指定协程运行于特定的一个线程、一个线程池或者不指定任何线程(这样协程就会运行于当前线程)。coroutines-core中 CoroutineDispatcher 有三种标准实现Dispatchers.DefaultDispatchers.IODispatchers.MainDispatchers.Unconfined,Unconfined 就是不指定线程。

launch函数定义如果不指定CoroutineDispatcher或者没有其他的ContinuationInterceptor,默认的协程调度器就是Dispatchers.DefaultDefault是一个协程调度器,其指定的线程为共有的线程池,线程数量至少为 2 最大与 CPU 数相同。

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 interface Deferred : Job

3.4 Coroutine builders

CoroutineScope.launch函数属于协程构建器 Coroutine builders,Kotlin 中还有其他几种 Builders,负责创建协程。

3.4.1 CoroutineScope.launch {}

CoroutineScope.launch {} 是最常用的 Coroutine builders,不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器,例如在 Android 中常用的GlobalScope.launch(Dispatchers.Main) {}

fun postItem(item: Item) {
    GlobalScope.launch(Dispatchers.Main) { // 在 UI 线程创建一个新协程
        val token = requestToken()
        val post = createPost(token, item)
        processPost(post)
    }
}
3.4.2 runBlocking {}

runBlocking {}是创建一个新的协程同时阻塞当前线程,直到协程结束。这个不应该在协程中使用,主要是为main函数和测试设计的。

fun main(args: Array) = runBlocking { // 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 {
        // here we can use suspending functions using any assertion style that we like
    }
}
3.4.3 withContext {}

withContext {}不会创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成。

3.4.4 async {}

CoroutineScope.async {}可以实现与 launch builder 一样的效果,在后台创建一个新协程,唯一的区别是它有返回值,因为CoroutineScope.async {}返回的是 Deferred 类型。

fun main(args: Array) = runBlocking { // 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")
}

获取CoroutineScope.async {}的返回值需要通过await()函数,它也是是个挂起函数,调用时会挂起当前协程直到 async 中代码执行完并返回某个值。

4. 小结

Kotlin 协程可以极大地简化异步编程,虽然刚开始接触的时候学习比较吃力,但是接触过一段时间相信绝对会爱上它。建议大家也去浏览下面推荐的资料,可以更快地了解协程的大概。

推荐阅读:

  • Introduction to Coroutines(Roman Elizarov at KotlinConf 2017, slides)

  • Asynchronous Programming Techniques

  • Guide to kotlinx.coroutines by example

你可能感兴趣的:(Kotlin Coroutines(协程) 完全解析(一),协程简介)