Kotlin协程开篇

2021-04-09 10:29:48 浏览数 (1)

《Kotlin协程》均基于Kotlinx-coroutines 1.3.70

新开个坑,专门讲kotlin的协程。聊协程之前先说一下具体聊的是协程的什么内容。

· 协程是什么?

· 什么时候用协程?

· 协程的核心是什么?

· kotlin的协程和其他语言的协程有什么异同?

kotlin的协程的出现其实比kotlin语言还晚一点。在当前这个版本,协程甚至都还处于一个不稳定的迭代版本中。协程到目前为止都还没进入kotlin的标准库,它是一个独立的依赖库,叫 Kotlinx。对于想在开发中使用协程的人来说,需要在依赖里加入kotlinx-core依赖。作为一个独立的依赖包,它的源码可以从github上获取,《Kotlin协程》分析的源码就是以github上的master分支为参考。

协程没那么难

协程的出现是为了解决异步编程中遇到的各种问题。从高级编程语言出现的第一天,异步执行的问题就伴随出现。

在Kotlin里使用协程非常方便,

代码语言:javascript复制
import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 在后台启动一个新的协程并继续
        delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
        println("World!") // 在延迟后打印输出
    }
    println("Hello,") // 协程已在等待时主线程还在继续
    Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}

上面的代码是一个常规启动协程的方式,关键函数只有 launch,delay,这两个函数是kotlin协程独有的。其他函数都属于基本库。

代码的输出结果是

Hello, World!

这是一个典型的异步执行结果,先得到 Hello,而不是按代码顺序先得到 World。

异步执行在平时开发中经常遇到,比如执行一段IO操作,不管是文件读写,还是网络请求,都属于IO。

在Android中我们对IO操作的一个熟知的规则是不能写在主线程中,因为它会卡线程,导致ANR。而上面的代码其实是不会卡线程的。用同步的方式写异步代码 这句话在很多资料中出现过,划重点。

理解这句话的关键在于,协程干了什么,让这个异步操作不会卡主线程?

我们知道类似的技术在RxJava中也有,它通过手动切线程的方式指定代码运行所在的线程,从而达到不卡主线程的目的。而协程的高明和简洁之处在于,开发者不需要主动切线程。

在上面的代码中打印一下线程名观察结果。

代码语言:javascript复制
import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 在后台启动一个新的协程并继续
        println("Thread: ${Thread.currentThread().name}")
        delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
        println("World!") // 在延迟后打印输出
    }
    println("Thread: ${Thread.currentThread().name}")
    println("Hello,") // 协程已在等待时主线程还在继续
    Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}

我们会得到

Thread: main Hello, Thread: DefaultDispatcher-worker-1 World!

可以看到在打印World的时候,代码是运行在子线程的。

协程其实没那么容易

对于经常用协程开发的人来说,有几个很有意思的问题值得思考下。· 上面代码中的Thread.sleep()可以改成delay()吗?

· 为什么理论上可以开无限多个coroutine?

· 假设有一个IO操作 foo() 耗时a,一个计算密集操作 bar() 耗时b,用协程来执行的话,launc{a b} 耗时c,c是否等于a b?

另外一个很有意思的问题需要用代码来展示。在协程中有一个函数 runBlocking{},没接触过的可以简单理解为它等价于launch{}。

用它来改造上面的代码,

代码语言:javascript复制
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking


fun main() = runBlocking {
    GlobalScope.launch { // 在后台启动一个新的协程并继续
        println("Thread: ${Thread.currentThread().name}")
        delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
        println("World!") // 在延迟后打印输出
    }
    println("Thread: ${Thread.currentThread().name}")
    println("Hello,") // 协程已在等待时主线程还在继续
    Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}

我们会得到

Thread: DefaultDispatcher-worker-1 Thread: main Hello, World!

现在我们把 GlobalScope.launch这行改造一下,

代码语言:javascript复制
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    launch { // 在后台启动一个新的协程并继续
        println("Thread: ${Thread.currentThread().name}")
        delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
        println("World!") // 在延迟后打印输出
    }
    println("Thread: ${Thread.currentThread().name}")
    println("Hello,") // 协程已在等待时主线程还在继续
    Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}

现在再看执行结果,

Thread: main Hello, Thread: main World!

WTF? launch里的代码也执行在主线程了?

这个问题涉及到Kotlin协程的Scope,调度,也是协程的实现核心逻辑

Kotlin不是第一个提出协程的

实际上在Kotlin之前就有不少语言实践了协程这个概念。比如python,golang。

而最原始的协程其实不叫协程,叫纤程(Fiber)。听说过Fiber的人都已经。。

甲:听说过纤程吗 乙:Fiber是吧 甲:你今年起码40岁了吧

纤程是微软第一个提出的,但因为它的使用非常的反人类,对程序员的代码质量要求非常高,以至于没人愿意用它。虽然现在还可以在微软官网上找到关于纤程的资料,但能用好纤程的程序员凤毛麟角。

Using Fibers

直到golang的出现,才把协程这个技术发扬光大。有人说python也有协程呀,为什么是golang。其实python的协程不是真正意义上的协程,后面我们会说到。python的协程是基于yield关键字进行二次封装的,虽然在高层抽象上也是以函数作为协程粒度,但对比golang差的太远。

golang做了什么

golang的协程叫goroutine,跟kotlin的coroutine差不多。golang用一种程序员更容易理解的抽象定义了协程粒度goroutine,还有它的各种操作。

对于程序员来说,再也不用关心什么时候切协程,协程在什么线程运行这种问题,开发效率和代码运行效率得到成倍提升。

golang在编译器上做了很多优化,当代码中发生IO或者内核中断的时候,会自动帮你切协程。熟悉计算机原理的能明白,当发生内核中断的时候,比如请求一个磁盘文件,中断发生时CPU其实是没有工作的,执行逻辑在这个时候处于一个空转,直到中断返回结果才继续往下执行。

于是在中断发生的时候,CPU相当于浪费了一段时间。golang在这个时候切协程,就能把CPU浪费的算力利用起来交给另外一个协程去执行。

kotlin的协程还在发展

如果去看kotlin的协程源码的话会发现里面有很多 exeprimental 的api和实现逻辑。直到1.3.70为止,jetbain团队还在继续地为coroutine机制增加新的活力。目前来说coroutine处于一个稳定阶段,可以基于1.3.70版本来分析它,后面应该不会有很大机制上的变动了。

0 人点赞