Kotlin(一)掌握基础知识:数据、集合、库函数
Kotlin(二)掌握基础知识:字符串和异常处理
Kotlin(三)掌握基础知识:类
Kotlin(四)掌握基础知识:线程(Thread.kt)详解
协程是轻量级的线程,他降低了线程创建,线程切换,线程初始化的性能消耗;
协程具有以下几个特点
- 不是被操作系统内核所管理,而完全是由程序所控制;
- 协程在线程中是顺序运行的,协程的异步和并发操作是通过协程的挂起方法来执行的,协程挂起时不会阻塞线程;这点不同于线程,线程一旦挂起,该线程就会被阻塞;
- 协程运行在线程当中,一个线程中可以创建多个协程,每一个协程可以理解为一个耗时任务
协程的代码在 kotlinx.coroutines 中,这个包需要通过dependencies来引入进来;
我们先用IntelliJ IDEA来创建一个工程来练习协程的使用,其步骤如下:
-
创建空项目并添加模块
1)File -> New -> Project,选择Empty Project
2) 填入项目名称和项目位置,点击finish
3)在项目结构中选择Modules,完成模块添加,此处勾选了Java和Kotlin/JVM表示该模块会添加java和kotlin相关库
4)点击Next填入相关信息,点击OK完成项目的添加
5)模块添加完成后的项目结构如下:
其中build.gradle是在编译会用到的一些库 -
在build.gradle文件中添加协程需要用的库
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
}
- 新建kt文件,就可以正常使用协程了,此处新建了一个test.kt的文件
现在我们就来充分认识一下协程
- 创建协程:通过GlobalScope.launch来创建
fun main() {
GlobalScope.launch { // 在后台启动一个新的协程,协程的生命周期和应用程序生命周期绑定
delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
println("World!")
}
println("Hello,") // 协程已在等待时主线程还在继续
Thread.sleep(2000L) // 阻塞主线程 2 秒钟
/*
通过GlobalScope创建的新协程的生命周期受应用程序的生命周期限制,类似java中的守护线程,
所以当主线程运行结束之后,GlobalScope.launch方法创建的协程也就消失了
所以你会发现如果你将Thread.sleep(2000L)这行代码取消掉,你会发现只打印了Hello
这是因为主线程打印了Hello之后,主程序退出了,然后GlobalScope.launch启动的协程需要延迟1秒才打印,但是随着主线程的退出,协程也退出了,顾不打印World
*/
}
代码运行结果
Hello,
World!
GlobalScope实现了CoroutineScope接口,CoroutineScope接口表示一个协程的构造器,每一个协程的构造器都需要实现该接口,CoroutineScope接口里面包含了实现协程的上下文;
GlobalScope.launch定义在Builders.Common.kt文件中,该文件对CoroutineScope接口实现了一个扩展函数launch();
协程上下文包含一个 协程调度器 (CoroutineDispatcher)它确定了哪些线程或与线程相对应的协程执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行
通过GlobalScope创建的新协程的生命周期受应用程序的生命周期限制,类似java中的守护线程,所以当主线程运行结束之后,GlobalScope.launch方法创建的协程也就消失了
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
launch方法有三个参数,第一个参数context表示协程的上下文运行环境,第二个参数start表示协程的构造器该如何开始这个协程,第三个参数block是一个lambda表达式,表示协程运行主体;
launch方法方法返回Job实例,通过这个Job实例我们可以判断这个协程是否完成,取消等等操作,Job有如下几个关键方法:
- job.start() :启动协程
- job.join() :等待协程执行完毕
- job.cancel() :取消一个协程
- job.cancelAndJoin() : 等待协程执行完毕然后再取消
launch方法的函数体主要是根据start类型来创建一个协程,这个协程是LazyStandaloneCoroutine或者是StandaloneCoroutine,这两个类均实现了AbstractCoroutine抽象类,AbstractCoroutine才是真正实现协程的主类;
第一个参数,即协程的上下文运行环境,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。
fun main() = runBlocking<Unit> {
launch { // 运行在父协程的上下文中,即 runBlocking 主协程
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
/* 非受限调度器 vs 受限调度器
Dispatchers.Unconfined 协程调度器在调用它的线程启动了一个协程,但它仅仅只是运行到第一个挂起点。
挂起后,它恢复线程中的协程,而这完全由被调用的挂起函数来决定。非受限的调度器非常适用于执行不消耗
CPU 时间的任务,以及不更新局限于特定线程的任何共享数据(如UI)的协程。*/
launch(Dispatchers.Unconfined) { // 不受限的: 将工作在主线程中
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // 将会获取默认调度器
println("Default : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) { // 将使它获得一个新的线程
/* newSingleThreadContext 为协程的运行启动了一个线程。 一个专用的线程是一种非常昂贵的资源。 在真实的应用程序中两者都必须被释放,当不再需要的时候,使用 close 函数,或存储在一个顶层变量中使它在整个应用程序中被重用。*/
println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
}
输出结果为:
Unconfined : I'm working in thread main
Default : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking : I'm working in thread main
创建一个协程之后,这个协程的启动方法是由第二个参数start来决定的,其在CoroutineStart中定义了四种类型
- DEFAULT: 默认实现方式,表示创建协程之后立即运行
- LAZY:延迟启动协程,如可以通过start方法来运行协程
- ATOMIC:实现方式类似DEFAULT,区别是这个协程在运行之前不能被取消
- UNDISPATCHED
- 创建协程:通过runBlocking来创建
创建线程的另一个协程的方式是通过runBlocking函数,其实现方式为:
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
****
}
该函数和launch函数的不同点是会阻塞调用者线程直到协程完成,类似Thread.sleep()
fun main() {
GlobalScope.launch { // 在后台启动一个新的协程并继续
delay(1000L)
println("World!")
}
println("Hello,") // 主线程中的代码会立即执行
runBlocking { // 但是这个表达式阻塞了主线程
delay(5000L)
}
println("主线程运行结束")
}
上述代表表现形式为:先打印hello,然后通过runBlocking启动了一个协程,该协程会阻塞调用者即主线程,只有当runBlocking 函数体运行结束,即休眠5秒之后,开始打印"主线程运行结束"
- 创建协程:GlobalScope.async
async和launch的区别就是async带有返回值,其表示异步/并发执行协程;
fun main() {
runBlocking {
var deferred = GlobalScope.async {
println("协程1开始执行")
return@async "GlobalScope.async 1"
}
var deferred2 = GlobalScope.async {
println("协程2开始执行")
return@async "GlobalScope.async 2"
}
Thread.sleep(2000L)
if (deferred.isCompleted && deferred2.isCompleted) {
var result1 = deferred.await();
var result2 = deferred2.await();
println("协程执行完毕$result1 + $result2")
} else {
println("协程未执行")
}
}
}
输出为:
协程1开始执行
协程2开始执行
协程执行完毕GlobalScope.async 1 + GlobalScope.async 2
async函数返回一个Deferred类型,该类继承自Job,并且该类提供了wait函数,getCompleted函数等函数来获取协程返回值;其中getCompleted函数如果协程任务还没有执行完成则会抛出IllegalStateException
同理我们可以将async封装成一个函数
fun main() {
var deferred = GlobalScope.async {
asyncValue()
}
Thread.sleep(2000L)
if (deferred.isCompleted) {
var result = deferred.getCompleted();
println("协程执行完毕$result")
} else {
println("协程未执行")
}
}
fun asyncValue(): String {
return "GlobalScope.async"
}
async函数可以通过将 start 参数设置为 CoroutineStart.LAZY 而变为惰性的。 在这个模式下,只有结果通过 await 获取的时候协程才会启动,或者在 Job 的 start 函数调用的时候
- suspend修饰符
suspend意为暂停,表示该方法是一个挂起函数,从而在不阻塞的情况下执行其他工作;同时只有suspend函数才能调用susend函数
import kotlinx.coroutines.*
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
println("start")
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
println("doSomethingUsefulOne")
delay(3000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
println("doSomethingUsefulTwo")
delay(5000L) // pretend we are doing something useful here, too
return 29
}
输出为:
/*
主协程先打印start,然后打印doSomethingUsefulOne,延迟3秒之后打印doSomethingUsefulTwo,然后再延迟5秒打印The answer is 42
*/
start
doSomethingUsefulOne
doSomethingUsefulTwo
The answer is 42
Completed in 8007 ms
可见通过suspend修饰符修饰的方法两者之间是没有依赖,按照顺序执行;