前言
在上一篇中,讲解了关于Kotlin协程对应的释放资源、超时、组合挂起函数相关知识点。在这一篇中,将会讲解Kotlin协程对应的同步,以及初探协程上下文以及调度器。
话不多说,直接开始!
先看上一篇例子
suspend fun doSomethingUsefulOne(): Int {
println("doSomethingUsefulOne")
//所有kotlinx.coroutines中的挂起函数都是可被取消的。
delay(1000L)
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
println("doSomethingUsefulTwo")
delay(1000L)
return 29
}
//使⽤ async 并发
fun main() = runBlocking {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
//这⾥快了两倍,因为两个协程并发执⾏。请注意,使⽤协程进⾏并发总是显式的。
println("Completed in $time ms")
}
我们看到在协程里面使用async {}
让闭包里面的方法有了同步的作用!
那么在协程外面是否能使用async {}
让闭包里面的方法有同步作用呢?
当然有!
1、async风格的函数
1.1 无崩溃情况
suspend fun doSomethingUsefulOne(): Int {
println("doSomethingUsefulOne")
//所有kotlinx.coroutines中的挂起函数都是可被取消的。
delay(1000L)
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
println("doSomethingUsefulTwo")
delay(1000L)
return 29
}
//这不是挂起函数
fun doSomethingUsefulOneAsync() = GlobalScope.async {
doSomethingUsefulOne()//这是挂起函数
}
//这不是挂起函数
fun doSomethingUsefulTwoAsync() = GlobalScope.async {
doSomethingUsefulTwo()//这是挂起函数
}
//async⻛格的函数
fun main(){ //注意这里并没有 runBlocking {} 所以这里不在协程闭包里,而在main函数入口这!
val time = measureTimeMillis {
val one = doSomethingUsefulOneAsync()//非挂起函数可直接在非协程调用
val two = doSomethingUsefulTwoAsync()
//等待结果必须调⽤挂起或者阻塞
//这⾥我们使⽤ `runBlocking { …… }` 来阻塞主线程
runBlocking {
println("The answer is ${one.await() + two.await()}")
}
}
println("Completed in $time ms")
}
这里我们看到使用了GlobalScope.async {}
,在对应闭包里面调用了挂起函数,返回的缺是非挂起函数。所以在main主线程里面可以直接调用对应非挂起函数,但因为计算过程却在挂起函数里面,而这并非协程区域,所以需要使用 runBlocking { …… }
来阻塞主线程。
来看看运行效果:
doSomethingUsefulTwo
doSomethingUsefulOne
The answer is 42
Completed in 1124 ms
我们看到使用这种方式,依然能够达到对应的效果!
但是!!!!!!!这种使用方式强烈不推荐!!!!!
为啥不推荐?来看下一个例子
1.2 有崩溃情况
suspend fun doSomethingUsefulOne(): Int {
println("doSomethingUsefulOne")
delay(3000L) //改动1 这里改成了等待3秒
println("doSomethingUsefulOne over")
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
println("doSomethingUsefulTwo")
delay(1000L)
return 10 / 0 //改动2 这里改成了必崩溃的代码
}
//这不是挂起函数
fun doSomethingUsefulOneAsync() = GlobalScope.async {
doSomethingUsefulOne()//这是挂起函数
}
//这不是挂起函数
fun doSomethingUsefulTwoAsync() = GlobalScope.async {
doSomethingUsefulTwo()//这是挂起函数
}
//async⻛格的函数
fun main(){ //注意这里并没有 runBlocking {} 所以这里不在协程闭包里,而在main函数入口这!
val time = measureTimeMillis {
val one = doSomethingUsefulOneAsync()//非挂起函数可直接在非协程调用
val two = doSomethingUsefulTwoAsync()
//等待结果必须调⽤挂起或者阻塞
//这⾥我们使⽤ `runBlocking { …… }` 来阻塞主线程
runBlocking {
println("The answer is ${one.await() + two.await()}")
}
}
println("Completed in $time ms")
}
这里我们看到在doSomethingUsefulOne
里面,将挂起时间改成了3秒;其次在doSomethingUsefulTwo
里面加入了必崩溃的代码。
来看看运行效果:
doSomethingUsefulOne
doSomethingUsefulTwo //在这里等待了3秒才出现后面的日志!
doSomethingUsefulOne over //等待3秒后,这条日志以及过后的报错日志才打印出来!
Exception in thread "main" java.lang.ArithmeticException: / by zero
从这个运行效果可以看出:
doSomethingUsefulOne
这个方法挂起了3秒,无崩溃代码;doSomethingUsefulTwo
这个方法挂起了1秒,有崩溃代码;doSomethingUsefulOne
与doSomethingUsefulTwo
这两个方法是同步执行的;- 当
doSomethingUsefulTwo
这个方法崩溃时,没有第一时间提示错误信息,而是等待了3秒; - 换句话说,
doSomethingUsefulOne
这个方法挂起的时间越长,那么报错的信息将会等待越久!
那这种方式很明显是一个大坑啊!所以说:千万!千万!千万!别使用这种方式!!!
当然作为面试官的时候,可以适当“折磨”下应聘者。
那么该使用什么方式呢?
2、使用 async 的结构化并发
2.1 无崩溃情况
suspend fun doSomethingUsefulOne(): Int {
println("doSomethingUsefulOne")
//所有kotlinx.coroutines中的挂起函数都是可被取消的。
delay(1000L) //这里改为上面无崩溃情况时的原数据
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
println("doSomethingUsefulTwo")
delay(1000L)
return 29 //这里改为上面无崩溃情况时的原数据
}
//使⽤ async 的结构化并发
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
fun main() = runBlocking {
val time = measureTimeMillis {
println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")
}
代码分析:
- 首先这里main函数还是改成了主协程;
- 其次使用了
coroutineScope {}
对应的闭包里面填充了昨天例子里的代码; - 将函数变成了挂起函数,返回值就是对应方法同步后的和
来看看运行效果
doSomethingUsefulOne
doSomethingUsefulTwo
The answer is 42
Completed in 1075 ms
运行总花时和上面差不多,那看看对应崩溃后的情况呢?
2.2 有崩溃情况
suspend fun doSomethingUsefulOne(): Int {
println("doSomethingUsefulOne")
//所有kotlinx.coroutines中的挂起函数都是可被取消的。
delay(3000L)
println("doSomethingUsefulOne over")
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
println("doSomethingUsefulTwo")
delay(1000L)
return 10 / 0
}
//使⽤ async 的结构化并发
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
fun main() = runBlocking {
val time = measureTimeMillis {
println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")
}
这里崩溃情况的代码和上面改成一样了,doSomethingUsefulOne
挂起3秒,doSomethingUsefulTwo
必崩溃!
来看看运行效果
doSomethingUsefulOne
doSomethingUsefulTwo //等待1秒后,直接报错 因为delay(1000L) 挂起了一秒
Exception in thread "main" java.lang.ArithmeticException: / by zero
从这个运行效果上看,当第二个挂起方法报错时,第一个挂起方法也直接终止运行了!这就是正常想要的效果!
3、初探协程上下文以及调度器
协程总是运⾏在⼀些以 CoroutineContext 类型为代表的上下⽂中。协程上下⽂是各种不同元素的集合,其中主元素是协程中的 Job。
所有的协程构建器诸如 launch 和 async 接收⼀个可选的 CoroutineContext 参数,它可以被⽤来显式的为⼀ 个新协程或其它上下⽂元素指定⼀个调度器。
来看看下面的例子
3.1 所有调度器
fun main() = runBlocking<Unit> {
//当调⽤ launch { …… } 时不传参数,它从启动了它的 CoroutineScope 中承袭了上下⽂(以及调度器)。
//在这 个案例中,它从 main 线程中的 runBlocking 主协程承袭了上下⽂。
launch {
delay(1000)
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
// 不受限的——将⼯作在主线程中
// ⾮受限的调度器⾮常适⽤于执⾏不消耗 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")) {
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
从这运行效果可知:
- 当调⽤ launch { …… } 时不传参数时,它从 main 线程中的 runBlocking 主协程承袭了上下⽂。即使挂起了1秒或者调用了挂起函数,它依旧为主线程;
- 当参数为:
Dispatchers.Unconfined
不受限的——仍在在主线程中;
那么受限的呢?会怎样??
3.2 非受限调度器 vs 受限调度器
fun main() = runBlocking<Unit> {
//协程可以在⼀个线程上挂起并在其它线程上恢复。
launch(Dispatchers.Unconfined){
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
delay(500L)
println("Unconfined : After delay in thread ${Thread.currentThread().name}")
}
launch {
println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
delay(1000)
println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
}
}
运行效果
Unconfined : I'm working in thread main
main runBlocking: I'm working in thread main
Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor //这里变成子线程了
main runBlocking: After delay in thread main //即使挂起后,仍然为主线程
从这个运行效果可知:
- 无参数的调度器,即使挂起后,仍然为主线程!适合UI更新操作;
- 参数为:
Dispatchers.Unconfined
的调度器,在挂起前为主线程,挂起后就变成了子线程!不合适UI更新操作 - 因此协程可以在⼀个线程上挂起并在其它线程上恢复。
既然协程可以在⼀个线程上挂起并在其它线程上恢复,那么该如何进行调试呢?
3.2 调试协程与线程
3.2.1 工具调试
如图所示
每次调试时都需要手动鼠标左键那个选项,才会弹出下面的提示
这里对其状态分别进行介绍
- 第一个协程具有SUSPENDED状态 - 它正在等待值,以便可以将它们相乘。
- 第二个协程正在计算a值——它具有RUNNING状态。
- 第三个协程具有CREATED状态并且不计算 的值b。
但笔者调试时,下面那个状态不会随着调试改变而改变,只能关闭下面内容,重新执行上面鼠标左键,再次打开下面内容才会发生状态改变。
也不知是我操作不对还是啥的!就暂且知道有这个东西吧。
3.2.2 日志调试
fun log(msg:String) = println("[${Thread.currentThread().name}] $msg")
//⽤⽇志调试
fun main() = runBlocking<Unit> {
val a = async {
log("I'm computing a piece of the answer")
6
}
val b = async {
log("I'm computing another piece of the answer")
7
}
log("The answer is ${a.await() * b.await()}")
}
这里很简单,就是定义了一个方法,在打印前,将当前线程打印出来而已
运行效果
[main] I'm computing a piece of the answer
[main] I'm computing another piece of the answer
[main] The answer is 42
结束语
好了,本篇到这里也结束了!相信你更进一步掌握了协程相关的知识点!下一篇将深度讲解协程上下文以及调度器相关的知识点!