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<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) {
    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<out T> : 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<String>) = 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<String>) = 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 协程可以极大地简化异步编程,虽然刚开始接触的时候学习比较吃力,但是接触过一段时间相信绝对会爱上它。建议大家也去浏览下面推荐的资料,可以更快地了解协程的大概。

推荐阅读:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容