服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

Kotlin中的挂起与恢复,到底是什么?

日期: 来源:郭霖收集编辑:史大拿


/   今日科技快讯   /

近日,清华技术成果转化的公司智谱AI开源了GLM系列模型的新成员“中英双语对话模型ChatGLM-6B”,支持在单张消费级显卡上进行推理使用。这是继此前开源GLM-130B千亿基座模型之后,智谱再次推出大模型方向的研究成果。与此同时,基于千亿基座模型的ChatGLM也同期推出,初具问答和对话功能。

据悉,该模型基于 General Language Model (GLM) 架构,具有62亿参数。结合模型量化技术,用户可以在消费级的显卡上进行本地部署。

/   作者简介   /

本篇文章来自史大拿的投稿,文章主要通过分析具体案例来加深对协程状态的相关理解,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

史大拿的博客地址:
https://juejin.cn/user/2251439606079277

/   协程执行流程理解   /

还是老套路,先来看原始效果。

参考图1


在这里我们知道 runBlocking 会阻塞主线程来等待子协程的执行,我们通过了 kotlin 提供的 API 中的 measureTimeMillis 方法来计算出 runblocking 的执行时间。

kotlin 协程本质就是一个线程框架,我们在使用线程的时候,通过调用 Thread#start() 来通知 JVM 我们需要执行一个任务,然后 JVM 去调度执行。协程也是类似的,在协程中也有调度的概念。

在之前的文章中我们提到过,只要是 suspend 函数,一定是异步的,除非自己想不开调用 Thread.sleep。这也是学习协程我认为最重要的一点。首先我们要知道 launch{} 中会做哪yu些事情,通过 CoroutineScope#launch{} 创建一个协程的时候,首先会进入到调度前准备阶段,也就是开启一个协程。

开启一个协程的时候并不是执行阶段,只会将这个协程标记为活跃状态,此时这种状态是不会执行协程体中的代码。

先有调度,在有执行

参考图2


  • Job#isActivte 协程是否活跃
  • Job#isCancelled 协程是否被取消
  • Job#isCompleated 协程是否执行完成协程体

因为协程需要调度,并且这段代码是运行到 main 线程的,所以这段代码默认会执行调度前的代码,然后才是执行调度后(执行阶段)的代码,这段代码是运行在Main线程中的,如果我们将他放入IO线程,看看会有什么变化。

参考图3


可以看出 Main 线程和 IO 线程打印出来的执行顺序是不一样的,这一点也好理解,因为子协程会跟随父协程的线程,所以子协程中打印就会有时快有时慢。这取决于你 CPU 的运行速度,但是有一点,即使是 IO 线程,也会有调度的说法。

这里有细心的朋友也可能会看到,按照上面的说法,在这里代码3先执行,然后再执行的代码5,那么为什么此时 Job 还是活跃,未完成状态呢?

这是因为他虽然执行完了代码块中的代码,但是还是挂起状态,直到恢复之后才是完成状态!有了这个前提,接下来步入到本篇的核心,理解挂起于恢复

/   案例   /

我没有学习协程之前和学一段时间协程后看到挂起与恢复这两个字也很陌生。因为我们知道,挂起是通过 suspend 关键字,来标记的,但是恢复,怎么恢复?谁来恢复?

这里先剧透一下后面的内容,不深究,先有个概念就好。我们都知道 kotlin 是可以反编译为 java 代码的,但是 java 中并没有 suspend 关键字,那么我们要在 java 中调用 kotlin 中的 suspend 函数,就会要传入一个 Continuation 的东西,其本质就是一个回调。简单地说就是 : suspend 关键字就是 Continuation。如果你看过 Continuation 这个类就知道,恢复其实指的就是 Continuation#resumeWith。因为我们并没有 Continuation 对象,我们只有 suspend 关键字,所以恢复的代码全部都是 kotlin 帮我们完成的。恢复其本质就是 kotlin 帮我们调用 Continuation#resumeWith。

反编译看看:


来看看chatGPT怎么说:


好了,这里就不展开讲了,后面在聊这些,直到这行代码,你只要清楚,恢复的工作,是不需要我们的,是 kotlin 帮我们完成的。就像是开自动挡车一样,不需要踩离合,并且也没有离合。接下来看一些案例,来更好的理解挂起于恢复,我们都知道 delay() 是挂起函数,那么我们通过比较 Thread.sleep 与 delay 的区别来理解挂起于恢复。

案例1

sleep

delay

sleep() 代码应该比较好理解,就是正常代码阻塞,我不执行完,你不准执行。就好像是地铁安检一样,如果前面的那个人身上有危险物品,那么安检员就一直等待着,直到排除这个人没问题之后再放行。

delay() 则不同,delay 是一个挂起函数,当他挂起的时候,,会恢复现在已经挂起的函数,也就是会执行代码3中的代码。这也就是我为什么说 suspend 函数中的代码永远是异步的。delay() 中的代码很简单,也很重要,对理解挂起于恢复有很大的帮助,建议反复琢磨!

案例2

再来看一个 suspend 不会阻塞的代码。

阻塞:用时3s

sleep

非阻塞::用时2s

delay

sleep 就不用过多介绍了,还是按顺序执行,并不会有任何变化。

delay 这里需要注意的是代码4比代码3执行的更快!并且总耗时为2s,这就证明了两个协程之前并不会有关联,他们都是异步执行的,虽然在同一个 Main 线程内!说白点就是在 Main 线程内异步执行代码!如果是线程,要写这样的功能该怎么写呢?先thread 然后执行完异步任务之后再通过 handler 切换到 Main 线程。

案例3

接下来,就不看结果了,你能正确打印出他的执行流程嘛?

 fun main() {
    println("main start")  // 1

    runBlocking<Unit> {
        println("runBlocking start")  // 2
        delay(2000)
        println("runBlocking end") // 3
    }

    println("main 执行中..") // 4

    Thread.sleep(2100)
    println("main end") // 5
}

这段代码比较简单,我们从第一篇就开始说, runBlocking 会阻塞主线程来等待自己子协程执行,这里没有子协程,只有一个delay,所以会等待delay执行完再往下执行。所以这里的顺序为:

1,2,3,4,5

案例4

fun main() = runBlocking<Unit> {

    println("main start") // 1

    val job = launch {     // 协程1
        println("launch 1 start") // 2
        delay(1000L)  // TODO 延时1
        println("launch 1 end")  // 3
    }

    println("main mid")  // 4

    val job2 = launch {   // 协程2
        println("launch 2 start") //5
        delay(1000L) // TODO 延时2
        println("launch 2 end") //6
    }

    delay(1500) // TODO 延时3
    println("main end") // 7
}

这里难度稍微升级了,在这块代码中,一定是先执行调度前的代码。也就是1,4和 延时3。因为延时3会恢复正在挂起的函数执行,所以会执行协程1和协程2中的代码。那么首先肯定先执行2。敲黑板了,协程始终是异步的,所以这里执行完2,就会执行 5。然后同时等待1秒。那么正确结果为:

1,4,2,5,3,6,7

案例5


这里理解的是 Job 状态的掌握,你可以打印一下 Job 的三种状态:

isActive,isCancelled,isCompleted

首先执行调度前的任务执行1,4。这始终都是不变的,但是执行到job.join()的时候,会等待协程1执行完,那么就会执行协程1中的代码。

现在的顺序为1,4,2,3。此时job的状态为

isActive = false,isCompleted = true

说明这个 job 已经执行完了,这个 job 生命已经结束了。那么将一个没有生命的 job 给到一个新的协程中,运行起来可想而知,这个协程也是没有生命的。所以协程2中的代码不会执行。最终正确结果为:

1,4,2,3,7

案例6


在这段代码中,重点就是协程2使用了父协程的 coroutineContext 与自己的DIspatchers.IO。由案例5知道,在 join 之前的代码执行结果都一样。

1,4,2,3

然后最开始执行协程2,在这段代码中,因为我们用了自己的 Dispatchers.IO,所以开启了一个 IO 子协程。最重要的是 coroutineContext[Job] 是什么状态。

因为这个 Job 是在 coroutineScope 内部使用的,很明显,Job 的状态:

isActive = true,isCompleted = false,isCancelled = false

然后会通过阻塞2阻塞线程2s。

这里的关键点:

  • coroutineContext[Job] 存活可以执行协程2中协程体的代码
  • Dispatchers.IO 不会阻塞线程

所以最终结果为:

1,4,2,3,5,6,7

这个例子稍微有点复杂,正常开发中碰到这种代码建议重构!

案例7


默认执行调度前的代码我就不过多说了,首先会执行1,7。然后通过阻塞1阻塞500ms,此时协程1是异步的,所以会执行2。这里有一个关键点。

一定要分清楚 Job 和 coroutineContext。协程1中传入的 Dispatchers.IO 是 coroutineContext,返回的 Job 异步并没有任何关系!!协程2使用了协程1的 job 并不会有任何作用,因为协程2不是异步的,所以他会等待阻塞1执行完毕后在执行。当阻塞1执行完的时候,然后立即 Job#cancel() 了。所以协程1,协程2,协程3都会被 cancel 掉。最终的执行结果为:

1,7,2,9

案例8

// TODO ======== 案例8 ===================
fun main() {
    val useTime = measureTimeMillis {
        runBlocking<Unit> {
            println("main start") // 1

            val job = GlobalScope.launch { // TODO 全局协程
                println("launch 1 start") // 2
                delay(1000L) // 延迟1
                println("launch 1 end") // 3
            }

            println("main mid") // 4
            launch(context = job) { // TODO 子协程
                println("launch 2 start") // 5
                delay(2000L) // 延迟2
                println("launch 2 end") // 6
            }

            println("main end") // 7
        }
    }

    println("使用时间: $useTime")
}

这段代码有点刁钻,刚写出来的时候,我自己都没看明白是怎么回事,然后细细品味了一番才悟了!

首先执行调度前的代码是没问题的:

1,4

但此时并不会执行7,因为在执行子协程的时候使用到了 job,既然使用到了另一个 job,那么就必须把全局协程执行完才有 job。所以这里会先执行2, 然后遇到延迟1。所以目前的打印顺序为:

1,4,2

然后子协程被创建,并不会执行协程体中的代码,因为当前调度前的代码还没有走完
,所以会先执行调度前的代码。

1,4,2,7

最终你认为会执行全局作用域和子协程体中的代码?

那就大错特错了!!!

还记得 runBlocking 是干什么用的嘛?runBlocking 只会等待自己子协程执行。GlobalScope#launch{} 很显然不是 runBlocking 的子协程。那么子协程肯定是 runBlocking 的子协程吧,是的,没问题。可是这里的 job,是使用的 GlobalScope 的 job。

job的作用是什么呢?

管理协程的生命周期等。runBlocking 都不会等待 GlobalScope 作用域,更不会等待使用 GlobalScope job 的作用域。所以这里的最终结果为:

1,4,2,7,5

完整代码如下所示:
https://gitee.com/lanyangyangzzz/coroutine-project/blob/main/app/src/main/java/com/szj/coroutine/project/jvm/blog3/Client.kt

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
Android 14 Developer Preview一览
Kotlin Flow响应式编程,StateFlow和SharedFlow

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注


相关阅读

  • 网易二面:CPU狂飙900%,该怎么处理?

  • 点击“终码一生”,关注,置顶公众号每日技术干货,第一时间送达!耗时8个月联合打造 《 2023年Java高薪课程 》,已更新了 102G 视频,累计更新时长 500+ 个小时,需要的小伙伴可以了解下
  • 【第2883期】原生 CSS Custom Highlight 终于来了~

  • 前言平时阅读会划词,这个功能就很好用了。今日前端早读课文章由 @XboxYan 分享,公号:前端侦探授权。@XboxYan,阅文体验设计部,专注用户体验相关,热爱 CSS,热爱原生,github:https://gi

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 永久免费白嫖!功能超全,简直不要太香!

  • 大家好,我是你们的好朋友大明,欢迎大家来到【大明青年】。今日分享大家好,不知道大家有没有PDF上的烦恼,比如要转换PDF格式或者PDF转换为其他格式,还有一些公司或者个人机密的PDF
  • Kotlin中的挂起与恢复,到底是什么?

  • / 今日科技快讯 /近日,清华技术成果转化的公司智谱AI开源了GLM系列模型的新成员“中英双语对话模型ChatGLM-6B”,支持在单张消费级显卡上进行推理使用。这是继此前开源GLM
  • 汫洲中学嵌塑创意工作坊:传承工艺 变废为美

  •   在日前举行的第三十三届潮汕星河奖颁奖颁发大会上,汫洲中学嵌塑创意工作坊获文艺奖二等奖。汫洲中学学生是如何将地域文化与传统工艺结合起来,把一件件废弃物变为妙趣横生