Android—Kotiln进阶教程(四)

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

前言

在上一篇中,讲解了关于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
复制代码

我们看到使用这种方式,依然能够达到对应的效果!

但是!!!!!!!这种使用方式强烈不推荐!!!!!

扫描二维码关注公众号,回复: 13276133 查看本文章

为啥不推荐?来看下一个例子

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秒,有崩溃代码;
  • doSomethingUsefulOnedoSomethingUsefulTwo这两个方法是同步执行的;
  • 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 工具调试

1.png

如图所示

每次调试时都需要手动鼠标左键那个选项,才会弹出下面的提示

这里对其状态分别进行介绍

  • 第一个协程具有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
复制代码

结束语

好了,本篇到这里也结束了!相信你更进一步掌握了协程相关的知识点!下一篇将深度讲解协程上下文以及调度器相关的知识点!

猜你喜欢

转载自juejin.im/post/7032585913213386789