导航
Kotlin中的协程,用起来原来这么简单?
协程和线程
线程:
在Java中,线程大家应该都很熟悉了,我就不做过多的解释了。java中的线程实现是和操作系统中的线程是一对一的关系,所以在java中线程的创建和销毁是很重量级的操作,当然使用线程池是一种很好的优化方案。
协程
众所周知,线程可以说是轻量级的进程,那么协程就可以说是轻量级的线程。在Java中线程受操作系统调度,jvm对线程没有太多控制权,而协程的控制权是掌握在程序本身的,是可以用代码控制的。不仅如此,协程的创建和销毁是非常轻量级的操作,有多轻量级呢?在Kotlin中,协程就是一个对象实例,这下明白了吧,创建协程就像创建普通对象一样简单。
协程的使用
准备工作
首先你得要有一定的Kotlin语言基础,不懂的地方可以去Kotlin中文官网学习
要在Kotlin中使用协程,首先在项目中导入Kotlin协程核心库,kotlinx-coroutines-core,Kotlin协程核心库是使用Kotlin协程必须导入的,当然还可以根据项目的不同再导入其他扩展库。这里仅需要导入协程核心库即可。
开始使用
启动一个协程
启动一个协程的方法可多了去了,Kotlin提供了很多启动协程的扩展方法,这里就讲讲常用的几种,直接上代码:
fun main() = runBlocking {
println("runBlocking协程")
GlobalScope.launch {
println("GlobalScope.launch协程")
}.join() // 这里的join现在不理解没关系
GlobalScope.async {
println("GlobalScope.async协程")
}.await() // 这里的await现在不理解没关系
}
执行结果如下:
runBlocking协程
GlobalScope.launch协程
GlobalScope.async协程
我们重点看runBlocking,GlobalScope.launch,GlobalScope.async这三个方法。这三个方法中任一方法执行都会创建并启动一个协程。
首先看runBlocking的方法定义:
public fun <T> runBlocking(...): T
方法参数暂时不用管,所以这里省略了,runBlocking方法是一个泛型方法,并且是Kotlin中的顶层方法,在任何地方调用runBlocking方法后都会创建一个运行在当前线程的协程,直到runBlocking方法返回后才会运行后面的代码逻辑。由于runBlocking的这个特性,runBlocking的用途很有限。
再来看一下GlobalScope.launch方法定义:
public fun CoroutineScope.launch(...): Job
同样省略了方法参数,CoroutineScope.launch也是一个Kotlin中的顶层方法,啥,方法名错了?说好的GlobalScope.launch呢?并没有错,GlobalScope.launch调用的就是CoroutineScope的扩展方法launch,不信你看GlobalScope类定义:
public object GlobalScope : CoroutineScope
GlobalScope继承自CoroutineScope, 所以GlobalScope.launch调用的就是CoroutineScope.launch方法,从方法定义可以看到,它返回的是一个Job对象,这里先不用深究Job对象有什么用,你暂且只要知道它可以用来启动或者取消这个协程就行了。
接下来看GlobalScope.async方法定义:
public fun <T> CoroutineScope.async(...): Deferred<T>
按照惯例,省略方法参数,聪明的小伙伴一定猜到了,GlobalScope.async调用的就是CoroutineScope.async方法。没错,就是这么回事。CoroutineScope.async同样是Kotlin的顶层方法。也是一个泛型方法,从方法定义可知它返回一个Deferred对象,那这个Deferred对象是干嘛的呢,Deferred翻译过来就是延期的意思,这个对象有一个重要的方法就是await方法,这个方法被调用后,如果结果已经可用,那么会直接返回结果,如果结果不可用,那么会暂停调用方协程,直到await结果可用返回或者有异常从中抛出。
上面说了这么多次顶层方法,那么为什么要定义成顶层方法呢?因为在Kotlin中顶层方法是可以在任何地方都能调用的,当然前提是权限允许。这样就可以在任何地方创建协程了。以上就是在Kotlin中启动一个协程的方法,当然启动协程的方法b不只这些,这里只是介绍了一部分而已。
runBlocking,阻塞线程的协程
调用runBlocking方法后会创建并启动一个协程,并且只有待到runBlocking中的代码逻辑全部执行完成后,runBlocking后面的代码逻辑才会得到执行,来,看个例子:
fun main() {
println("我在runBlocking之前 ${
System.currentTimeMillis()}")
runBlocking {
println("我在runBlocking之中 ${
System.currentTimeMillis()}")
delay(2000) // 在协程中调用该方法会暂停当前协程指定时间,并不会阻塞线程
}
println("我在runBlocking之后 ${
System.currentTimeMillis()}")
}
执行结果:
我在runBlocking之前 1588400909039
我在runBlocking之中 1588400909040
我在runBlocking之后 1588400911043
从结果不难看出runBlocking之后的代码是在runBlocking结束之后才得到执行的,所以可以得出结论: runBlocking会阻塞当前线程直到协程中的任务执行完成才会返回继续执行runBlocking后面的代码逻辑。那么这样的阻塞式协程在何时会用到呢?这个问题其实很简单,runBlocking其实可以和main方法无缝衔接使用,以下写法可以说是runBlocking的典型用法了:
fun main() = runBlocking {
// 代码逻辑
}
像上面这种写法,在学习Kotlin协程时是很有用的,就是因为它会阻塞当前线程的这一特性。现在可能有人就会问: 既然有阻塞线程的协程,那应该有不阻塞线程的协程吧。答案是肯定的,其实绝大多数协程都是不会阻塞线程的,线程中的阻塞可以和协程中的暂停对应,但不能混作一谈,在协程中多个协程是依靠暂停来相互协作的。下面就来看看不阻塞线程的协程。
launch,不阻塞线程的协程
刚才我们讲了阻塞线程的协程,现在就讲讲不阻塞线程的协程。直接调用GlobalScope.launch就会创建并启动一个协程,并且该方法会直接返回。先看一段代码直观体验一下:
fun main() {
println("main方法开始")
GlobalScope.launch {
println("GlobalScope.launch启动")
delay(1000) // 防止协程过快结束
println("GlobalScope.launch结束")
}
println("main方法结束")
}
执行结果:
main方法开始
main方法结束
GlobalScope.launch启动
从执行结果来看,可以发现虽然协程的创建是在println("main方法开始")
和println("main方法结束")
之间,但是协程真正运行是在println("main方法结束")
之后,同时由于main方法结束,协程也就随之结束,只打印出了“GlobalScope.launch启动”,而没有后续了。像这样的协程就是不阻塞线程的协程。
假如你现在有这么一个需求,你现在需要执行一段非常耗时的操作,而当前线程有无法等待这么久,就像Android中的UI线程那样,像这样的问题你会怎么处理呢?答案很简单,相信你也很快就想到解决办法了,把耗时操作丢到子线程中去处理就行了呀,对就是这么简单粗暴。但有了协程之后,我们就可以这些写:
fun main() = runBlocking {
println("runBlocking开始")
launch {
var count = 0L
repeat(Int.MAX_VALUE) {
count++
}
println("耗时任务结束 count = $count")
}
println("runBlocking结束")
}
执行结果:
runBlocking开始
runBlocking结束
耗时任务结束 count = 2147483647