本文收录于 kotlin入门潜修专题系列,欢迎学习交流。
创作不易,如有转载,还请备注。
协程
从本篇文章开始,将会开启kotlin的另一个世界——协程,这是kotlin提供的异步处理机制。
提到异步,自然而然想起的就是多线程、多进程,这是绕不开的话题,但是本系列文章将暂时不阐述他们的区别,而是先将协程相关的知识阐述完毕后,在来做一个整体的对比以及剖析协程背后的原理。
本篇文章将会演示协程的基本使用方法。
Hello World!
本小节先来看一个协程的“hello world”。
首先,kotlin协程相关的接口是位于 kotlinx.coroutines包下的,因此,我们在使用协程的时候,要首先导入该包。但是该包并不位于kotlin标准库中(kotlin竟然没有提供标准库的协程支持),而是作为独立的库发布的,因此我们需要先引入协程相关的依赖。
在这里我们采用的是maven引入的方式,如下所示:
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>1.1.1</version>
</dependency>
需要注意,kotlin的版本一定要在版本1.3以上才可以使用,因为1.2版本中的协程还是实验性质的,所以推荐使用最新的kotlin版本(对于idea可以升级kotlin插件,如果无法升级则需要升级idea至最新版本),这样才能用到相对稳定的协程相关的接口方法。
如果是使用gradle作为构建工具,则可以像下面一样引入依赖:
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
}
这两种基本已经满足我们的使用需求了(maven常用于java工程,gradle则常用于android工程),其他构建方式就不再阐述。
下面来看下kotlin协程之“hello world”示例,如下所示:
fun main(args: Array<String>) {
GlobalScope.launch {
delay(1000L)
println("world!")
}
println("hello ")
Thread.sleep(2000L)
}
上面的代码执行完成后,打印结果如下所示:
hello
world!
GlobalScope.launch表示启动一个后台协程,该协程的任务是延时1000ms后打印“world!”字符。当kotlin启动过该协程后,会继续执行其后面的代码,所以我们看到先打印出了“hello ”字符串。需要注意,我们在打印hello字符串语句之后又调用了Thread.sleep方法,并延时等待了2000ms(理论上只要稍微大于GlobalScope.launch中的延时时间即可),这么做的原因是保持jvm继续存活,否则程序执行完后(即main方法线程执行完后)就会被终止,而不会执行GlobalScope.launch中延时后的打印语句(后面会有新的机制可以不必这么做),这点和多线程不一样。此外,采用GlobalScope启动的协程(它是top-level级别的协程),其生命周期将会伴随整个应用程序的生命周期。
再来看一下delay方法,该方法的声明如下所示:
suspend fun delay(timeMillis: Long):Unit
该方法的意思是,延迟协程的执行,延迟时间由timeMillis决定,表示延迟的毫秒数。delay方法并不会阻塞线程,而仅仅会挂起当前协程,并在其指定延迟的时间到达时唤醒当前协程。
由上面代码可知,delay方法使用了suspend关键字进行修饰,故名思议,suspend修饰的方法表示该方法是个可挂起方法,只能用于协程中或者被suspend方法调用。
上面示例中,实际上用到了java中的Thread线程机制(用于阻塞main线程),其实kotlin本身也提供了一种机制来阻塞线程,如下所示:
fun main(args: Array<String>) {
GlobalScope.launch {
delay(1000L)
println("world!")
}
println("hello ")
runBlocking {
delay(2000L)
}
}
上面代码执行结果同样会打印“hello world!”,runBlocking的作用就是用于阻塞main线程,以保持jvm的存活(否则程序运行结束后会被回收)。注意,前面说过delay方法并不阻塞线程,所以这里不要误以为是delay方法在阻塞main线程,其背后的机制实际上是,main线程会等待runBlocking中的代码执行完成!
我们也可以结合runBlocking来采用多协程的方式完成上述功能,如下所示:
fun main(args: Array<String>) = runBlocking {
GlobalScope.launch {
delay(1000L)
println("world!")
}
println("hello ")
delay(2000L)
}
打印结果同上。这里需要注意,我们使用runBlocking方法启动了main协程,然后又在main协程中启动了一个新的后台协程(即GlobalScope.launch ),因为main协程中的delay方法运行在runBlocking方法中,所以这种写法,依然能起到“阻塞main线程”的目的(main线程需要等待runBlocking运行完成)。
runBlocking<Unit>中的Unit表示main方法的返回值,这里只不过是进行了显示指定,其默认返回值其实就是Unit。
使用delay方法可以起到延迟等待的作用,但是这显然不是一个好的实现方式,毕竟程序执行的时间有些时候并不是确定的。如果我们想实现健全的等待机制,可以利用kotlin为我们提供的job,如下所示:
fun main(args: Array<String>) = runBlocking {
val job = GlobalScope.launch {
delay(1000L)
println("world!")
}
println("hello ")
job.join()//这里会等待协程job执行完成
}
上面job的类型是kotlin为我们提供的接口Job,表示一个任务。后面文章会进行阐述。
结构化并发
在kotlin中,我们使用GlobalScope.launch启动的协程是top-level级别,虽然协程是轻量级的,但是依然会消耗资源。比如我们启动了很多协程,在运行的时候发生了未知错误,如果没有一个健全的机制回收这些资源,那么kotlin是不会自动帮我们处理的,针对这种情况,我们可以使用结构化并发。即像使用线程一样,在需要的时候创建,而不是创建top-level级别的协程,如下所示:
fun main(args: Array<String>) = runBlocking {
launch {
delay(500L)
println("world!")
}
println("hello ")
}
上面代码同样会打印'hello world',与前面不同的是,我们没有再让main协程等待一定的时间,也没有使用join机制。这是因为,launch创建的协程,其作用域属于runBlocking(参见下节作用域构造器),此时,runBlocking会等待在作用域内启动的所有协程的执行,直到他们全部完成才会结束。
对于上面的代码,我们还可以结合前面提到的suspend方法做进一步整理,那就是将launch中的语句抽象出来,作为一个单独的方法,如下所示:
fun main(args: Array<String>) = runBlocking {
launch {
printWorld()
}
println("hello ")
}
//这里必须定义为suspend方法,否则无法使用同时suspend的delay方法。
suspend fun printWorld() {
delay(500L)
println("world!")
}
作用域构造器(Scope builder)
上面提到的协程作用域,实际上就是由不同的作用域构造器决定的,除了使用系统提供的默认作用域构造器,我们同样可以自己定义作用域构造器。kotlin为我们提供了创建作用域构造器的方法:coroutineScope。使用该方法会创建一个新的协程作用域,coroutineScope和runBlocking很相似,他们都会等待其作用域内的所有协程执行完成后才会结束,但是coroutineScope不会像runBlocking那样阻塞线程。来看个例子:
fun main(args: Array<String>) = runBlocking {
launch {//launch属于runBlocking作用域
delay(200L)
println("Task from runBlocking")
}
//创建了一个新的协程作用域,属于runBlocking作用域
coroutineScope {
launch {//属于coroutineScope作用域
delay(300L)
println("Task from nested launch in coroutine scope")
}
delay(100L)
println("Task from coroutine scope")
}
//属于runBlocking作用域,该打印语句永远会在coroutineScope
//返回之后执行,但是如果和开始处的launch方法中的打印时机进行对比
//则取决于launch的delay时间
println("coroutine scope is over")
}
我们先分析下上面的代码:
首先,我们使用runBlocking创建了一个main协程,在其代码块中,我们使用launch启动了一个新协程,该协程作用域属于main协程,launch启动的协程执行时机将会有其内部的delay时间决定。
接着,我们使用coroutineScope创建了一个新的协程作用域,该协程会等待其作用域内的所有协程执行完成后才会结束。这意味着,在这个示例中,只有coroutineScope内部的所有协程执行完成之后,coroutineScope才会返回,也就是说 println("coroutine scope is over")
这条语句永远都会在coroutineScope返回之后才执行。
最后,再结合不同的delay时间就不难推测出打印结果,如下所示:
Task from coroutine scope
Task from runBlocking
Task from nested launch in coroutine scope
coroutine scope is over
Global 协程
前面已经多次提到了Global协程,这里在明确一下它的概念。
在kotlin中,Global协程相当于后台线程的概念,即Gloabal协程并不会保证进程的存活,应用程序结束了它就结束了,来看个例子:
fun main(args: Array<String>) = runBlocking {
GlobalScope.launch {
repeat(100) { i ->
println(i)
delay(100L)
}
}
delay(300L)
println("end...")
}
上面代码执行过后打印如下:
0
1
2
3
end...
也就是说,程序什么时候结束,取决于runBlocking方法delay的时间,这也就是文章刚开始的时候,我们使用Global协程,为什么要等待一定时间的原因。当然,看到这里,我们显然有很多解决方法了(比如将GlobalScope.launch改为launch),这里只是更进一步的说明一下GlobalScope协程的机制。
至此,本篇文章阐述完毕。