kotlin杂谈系列十一(可能有误)

13 篇文章 0 订阅
13 篇文章 0 订阅

Kotlin杂谈系列十 一(协程在异步程序中的运用)

  • 先回顾一下 协程是什么 : 协程是有多个入口的函数
  • 协程可以实现非阻塞调用,通过配置不同的上下文(线程)来实现并发和并行的操作
  • 这里先来谈谈之前学的 runBlocking 和 launch 的区别
    1. runBlocking的返回值是Unit 而 launch的返回值是Job(这个是用来等待任务完成 或者取消任务)
    2. runBlocking的调用是阻塞调用 而 launch是非阻塞调用
  • 但是我们想返回执行的结果的话 就可以依靠async() 和 await() 函数来执行
//这里引用了一个库(Klaxon)来解析Json对象 这部分可以暂时不管
class Airport(
    @Json(name = "Code") val code: String,
    @Json(name = "Name") val name: String,
    @Json(name = "Delay") val delay: Boolean,
    @Json(name = "Weather") val weather: String
) {
    companion object {
        fun getAirpotData(code: String): Airport? {
            val url = "http://192.168.56.1:8000/$code/airport.json" //这个是本地的服务器
            return Klaxon().parse<Airport>(URL(url).readText())
        }
    }
}

fun main() = runBlocking {
    val format = "%-10s%-10s%-20s%-10s"
    println(String.format(format, "Name", "Code", "Temperature", "Delay"))
    val time = measureTimeMillis {
        val list = listOf("001", "002", "003", "004")

        val airportData: List<Deferred<Airport?>> =
            list.map { code ->
           /*------------------------这里---------------------------*/
                async(Dispatchers.IO)/*注意这里*/ {
                    println("thread : ${Thread.currentThread()}")
                    Airport.getAirpotData(code)
                }
          /*----------------------*******-----------------------------*/
            }


        airportData.mapNotNull { anAirportData -> anAirportData.await() /*这里*/ }.forEach { airport ->
            println(String.format(format, airport.name, airport.code, airport.weather, airport.delay))
        }


    }

    println("时间: $time")


}
  • async() 返回一个Deferred这个未来对象 之所以叫未来对象是因为他在未来可能会接收到 lambda表达式的结果或者是失败的异常
  • async(Dispatchers.IO) 如果不指定协程上下文(一个线程池)的话就会在主线程(main)中执行虽然没有阻塞线程但是和顺序执行没有多大区别
  • 当我们调用 await() 的时候 协程的执行流会阻塞 不是线程阻塞是协程的执行流阻塞 协程的执行流 : 我的理解就是多个协程交替执行的那个流还是看看图吧
image-20220410203324248
  • 箭头就表示执行流 蓝色方块表示函数的部分代码 灰色的方块表示函数

  • 调用await函数的时候 : 就是阻塞执行流 就是让红色箭头不往下走 等待执行返回的结果

  • 以上纯属我的理解不一定对(所以请谨慎食用 以后可能会改)

  • 异常处理

  • launch() 函数不会将异常返回给调用方 所以得给他 注册一个异常处理程序 才能处理异常

val launchAirport = runBlocking {
    val handler = CoroutineExceptionHandler { context, ex ->
        println("Caugth : ${context[CoroutineName]},${ex.message}")
    }   /*这里就是创建了一个异常处理程序*/
    try {
        val codes = listOf("001", "002", "003", "004", "005")
        val jobs: List<Job> = codes.map { code ->
            //这里                             
            launch(Dispatchers.IO + CoroutineName("airports") + handler /*将创建好了的异常处理程序交给launch函数*/ + SupervisorJob()) {
                val airport = Airport.getAirpotData(code)
                println("${airport?.code} ${airport?.delay}")
            }

        }
        jobs.forEach { it.join() }
        jobs.forEach { println("Cancelled : ${it.isCancelled}") }
    } catch (e: Exception) {
        println("Error: ${e.message}")
    }
} 


fun main() = launchAirport
  • 你可能很奇怪 + handler 为什么是加号 这是什么意思 带着好奇的眼光我们追追源码吧
public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineContext, Throwable) -> Unit): CoroutineExceptionHandler

public interface CoroutineExceptionHandler : CoroutineContext.Element

public interface Element : CoroutineContext
  • 可以发现最上级是一个 CoroutineContext 接口 所以同种类型可以相加就不奇怪了 那相加是什么意思呢
public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }
  • 看不懂 ^_^ 有点emo了 那还是看看官方的解释吧
Returns a context containing elements from this context and elements from other context. The elements from this context with the same key as in the other one are dropped.

返回一个上下文,其中包含此上下文中的元素和其他上下文中的元素。该上下文中与另一个上下文中具有相同键的元素将被删除
  • 以后来详细谈谈

  • async就可以将异常的信息存储到那个未来对象中 (Deferred)

  • 如果子协程因失败而取消那么父协程默认也会取消

  • 但是可以在子协程中注册 SupervisorJob (父类型是 : CoroutineContext) 来让子协程取消但是父协程不取消

val asyncAirport = runBlocking {
    val codes = listOf("001", "002", "003", "004", "005")
    val airportDatas: List<Deferred<Airport?>> = codes.map { code ->
        async(Dispatchers.IO+ SupervisorJob()/*这里就指定了这个协程被取消了但是父协程不会因此而取消*/) {
            Airport.getAirpotData(code)
        }

    }

    for (i in airportDatas) {
        try {
            val airport = i.await()
            println("${airport?.code} ${airport?.delay}")
        }catch (e:Exception){
            println("Error: ${e.message}")
        }
    }
}
  • 层次结构的协程的规则
  1. 如果协程共享创建他的协程的上下文 那么这个协程就是子协程
  2. 父协程只有在其所有的子协程完成后才可以完成
  3. 取消父协程就是取消了其全部的子协程
  4. 已经进入挂起点的协程可能会受到从挂起点抛出的异常(CancellationException)
  5. 一个忙碌的 不在挂起点的协程 可以检查isActive属性来看他是否在忙碌是被取消
  6. 如果一个协程是忙碌的(执行任务)那么他是不会听从取消命名
  7. 如果协程有资源需要释放那么就的在协程的finally块中执行
  8. 未处理的异常会导致协程取消
  9. 如果子协程失败 默认就会导致父协程取消 从而导致兄弟协程被取消 可以使用监督作业(SupervisorJob())可以使得父协程到子协程的取消是单向的 即子不能取消父 父可以取消子
  • 补充

    什么是挂起 : 挂起就是一个将一个协程从他当前的操作的线程中剥离开 线程不再管这个协程了如果他还有任务就执行任务 如果没有的话就被回收利用了 而协程则是被挂起在另一个线程(我们指定的)中执行 执行完成后会被切回原来那个线程继续执行没有被挂起(suspend)修饰的代码块 可是如果原来的线程被回收了怎么办 没事kotlin帮我们记录了我们执行到哪里了,kotlin帮我们在开一个原来那个线程然后继续执行没有执行完了代码了这就是挂起

  • 但是 : 用关键词(suspend)修饰的函数自己不具备挂起的功能 挂起的功能是有kotlin的协程框架实现的 用suspend修饰的函数只是告诉使用者我是一个耗时的函数 请你把我放在协程中调用 而真正的挂起功能是kotlin实现的 所以我们不得不调用kotlin自带的挂起函数 这就是为什么官方的定义 : 一个挂起函数要么在协程中执行 要么在另一个挂起函数中执行 换句话说就是 无论过程怎么样 最终必须是在一个协程中调用的

  • 能执行挂起操作的就是 : yield() delay() await()

  • 取消和超时

  • 我们可能有时候不在乎协程中启动的任务是否完成,可以通过对 Job 和 Deferred<T> 调用cancel() 或者 cancelAndJoin()方法来取消 协程可能不会立即执行该任务 如果他在忙碌时(执行任务时)命令不会中断他

    如果是在协程定在挂起点的时候来取消的话那就是抛出一个异常来取消的**(CancellationException)**

  • 可以在执行任务的过程中检查 他的isActive属性来判断他是否被取消 被取消的话就停止任务 并接收取消

演示一下:

suspend fun compute(checkActive : Boolean) = coroutineScope {
    var count = 0L
    val max = 10000000000
    while(if (checkActive) isActive else (count<max)){
        count++
    }
    if (count == max){
        println("compute, checkActive : $checkActive ignored cancellation")

    }else{
        println("compute, checkActive : $checkActive bailed out early")
    }
}

runBlocking {
    val job = launch(Dispatchers.Default)/*指定的线程池中运行*/ {
        launch { compute(false) }
        launch { compute(true)}
    }

    println("let them run")
    Thread.sleep(100)
    println("OK, that's enough, cancel")
    job.cancelAndJoin()
}

//输出 :
let them run
OK, that's enough, cancel
compute, checkActive : true bailed out early
compute, checkActive : false ignored cancellation //这里
  • 可以看到 没有在执行过程检查 isActive 的操作不会服从我们的取消命令 coroutineScope这个是自带的挂起函数 只有这个才能真正具有挂起的功能 所以我们在他的代码块中包裹了我们要执行的逻辑

  • 拒绝取消 勿打扰

    有可能你在执行一个很重要的操作 任何命令都不能打扰你 如果你的关键代码中没有挂起点 那么就没事 可是如果你的代码里挂起点 那么就有事了 这时候就得利用 withContext(NonCancellable)来告诉他们这段代码无论怎么样都不能被取消

suspend fun doWork(id : Int, sleep : Long) = coroutineScope {
    try {
        println("$id : enter $sleep")
        delay(sleep)
        println("$id : finished nap $sleep")
        withContext(NonCancellable){
            println("id : $id do not disturb ,please")
            delay(5000)
            println("$id ok you can talk to me now")
        }
        println("$id outside the restricted context")
        println("$id :isActive : $isActive")
    } catch {
        println("$id : doWork($sleep) was cancelled")
    }

}


runBlocking {
    val job = launch(Dispatchers.Default) {
        launch { doWork(1,3000) }
        launch { doWork(2,100) }
    }
    Thread.sleep(1000)
    job.cancel()
    println("cancelling")
    job.join()
    println("done")
}

//输出
1 : enter 3000
2 : enter 100
2 : finished nap 100
id : 2 do not disturb ,please
cancelling
1 : doWork(3000) was cancelled
2 ok you can talk to me now
2 outside the restricted context
2 :isActive : false
done
  • 可以看到 1 在没进入勿打扰区域被取消抛了一个异常 2 进入了勿扰区域不能被打扰执行完后正常退出

  • kotlin的协程默认是双向取消 就是 一个一旦子协程发生异常或者因为其他原因取消了 那么父协程也会取消这会导致其他兄弟线程也取消 但是可以通过监督作业实现单向取消 即 父协程可以取消子协程 而 子协程不能取消父协程

  • 两个办法 一个是注册一个监督器 就是 SupervisorJob() 前面将过这里将第二种

  • 使用supervisorScope 来让他们受监督 受监督的子协程不能取消父协程 而父协程可以取消子协程

runBlocking {
    val handler = CoroutineExceptionHandler{_,ex->
        println("Exception handled : ${ex.message}")
    }

    val job = launch(Dispatchers.Default + handler) {
        supervisorScope/*这里*/ {
            launch { doWork(1,2000) }
            launch { doWork(2,1000) }
        }
    }
    Thread.sleep(1500)
    job.cancel()
    job.join()

}
  • 如果你想要在超时的情况下来取消协程可以使用 withTimeout()
  • 补充一下 join()函数 用于等待启动的协程完成
runBlocking {
    val handler = CoroutineExceptionHandler{_,ex->
        println("Exception handled : ${ex.message}")
    }

    val job = launch(Dispatchers.Default + handler) {
        withTimeout(1500)/*就像这样*/ {
            launch { doWork(1,2000) }
            launch { doWork(2,1000) }
        }
    }
    Thread.sleep(1500)
    job.cancel()
    job.join()


}
  • 以上就是我对协程的全部学习了 协程还有很多东西,我以后会慢慢来学 协程对我来说是一个新东西 我慢慢理解 慢慢学 上面的总结可能有误 请谨慎食用
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值