专栏名称: 狗厂
目录
相关文章推荐
51好读  ›  专栏  ›  狗厂

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

狗厂  · 掘金  ·  · 2018-04-20 02:46

正文

本文基于 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 就是不指定线程。







请到「今天看啥」查看全文