近日,清华技术成果转化的公司智谱AI开源了GLM系列模型的新成员“中英双语对话模型ChatGLM-6B”,支持在单张消费级显卡上进行推理使用。这是继此前开源GLM-130B千亿基座模型之后,智谱再次推出大模型方向的研究成果。与此同时,基于千亿基座模型的ChatGLM也同期推出,初具问答和对话功能。据悉,该模型基于 General Language Model (GLM) 架构,具有62亿参数。结合模型量化技术,用户可以在消费级的显卡上进行本地部署。本篇文章来自史大拿的投稿,文章主要通过分析具体案例来加深对协程状态的相关理解,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。https://juejin.cn/user/2251439606079277
在这里我们知道 runBlocking 会阻塞主线程来等待子协程的执行,我们通过了 kotlin 提供的 API 中的 measureTimeMillis 方法来计算出 runblocking 的执行时间。kotlin 协程本质就是一个线程框架,我们在使用线程的时候,通过调用 Thread#start() 来通知 JVM 我们需要执行一个任务,然后 JVM 去调度执行。协程也是类似的,在协程中也有调度的概念。在之前的文章中我们提到过,只要是 suspend 函数,一定是异步的,除非自己想不开调用 Thread.sleep。这也是学习协程我认为最重要的一点。首先我们要知道 launch{} 中会做哪yu些事情,通过 CoroutineScope#launch{} 创建一个协程的时候,首先会进入到调度前准备阶段,也就是开启一个协程。开启一个协程的时候并不是执行阶段,只会将这个协程标记为活跃状态,此时这种状态是不会执行协程体中的代码。- Job#isCompleated 协程是否执行完成协程体
因为协程需要调度,并且这段代码是运行到 main 线程的,所以这段代码默认会执行调度前的代码,然后才是执行调度后(执行阶段)的代码,这段代码是运行在Main线程中的,如果我们将他放入IO线程,看看会有什么变化。可以看出 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。好了,这里就不展开讲了,后面在聊这些,直到这行代码,你只要清楚,恢复的工作,是不需要我们的,是 kotlin 帮我们完成的。就像是开自动挡车一样,不需要踩离合,并且也没有离合。接下来看一些案例,来更好的理解挂起于恢复,我们都知道 delay() 是挂起函数,那么我们通过比较 Thread.sleep 与 delay 的区别来理解挂起于恢复。sleep() 代码应该比较好理解,就是正常代码阻塞,我不执行完,你不准执行。就好像是地铁安检一样,如果前面的那个人身上有危险物品,那么安检员就一直等待着,直到排除这个人没问题之后再放行。delay() 则不同,delay 是一个挂起函数,当他挂起的时候,,会恢复现在已经挂起的函数,也就是会执行代码3中的代码。这也就是我为什么说 suspend 函数中的代码永远是异步的。delay() 中的代码很简单,也很重要,对理解挂起于恢复有很大的帮助,建议反复琢磨!sleep 就不用过多介绍了,还是按顺序执行,并不会有任何变化。delay 这里需要注意的是代码4比代码3执行的更快!并且总耗时为2s,这就证明了两个协程之前并不会有关联,他们都是异步执行的,虽然在同一个 Main 线程内!说白点就是在 Main 线程内异步执行代码!如果是线程,要写这样的功能该怎么写呢?先thread 然后执行完异步任务之后再通过 handler 切换到 Main 线程。接下来,就不看结果了,你能正确打印出他的执行流程嘛? 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执行完再往下执行。所以这里的顺序为: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秒。那么正确结果为:这里理解的是 Job 状态的掌握,你可以打印一下 Job 的三种状态:isActive,isCancelled,isCompleted首先执行调度前的任务执行1,4。这始终都是不变的,但是执行到job.join()的时候,会等待协程1执行完,那么就会执行协程1中的代码。isActive = false,isCompleted = true说明这个 job 已经执行完了,这个 job 生命已经结束了。那么将一个没有生命的 job 给到一个新的协程中,运行起来可想而知,这个协程也是没有生命的。所以协程2中的代码不会执行。最终正确结果为:在这段代码中,重点就是协程2使用了父协程的 coroutineContext 与自己的DIspatchers.IO。由案例5知道,在 join 之前的代码执行结果都一样。然后最开始执行协程2,在这段代码中,因为我们用了自己的 Dispatchers.IO,所以开启了一个 IO 子协程。最重要的是 coroutineContext[Job] 是什么状态。因为这个 Job 是在 coroutineScope 内部使用的,很明显,Job 的状态:isActive = true,isCompleted = false,isCancelled = false- coroutineContext[Job] 存活可以执行协程2中协程体的代码
这个例子稍微有点复杂,正常开发中碰到这种代码建议重构!默认执行调度前的代码我就不过多说了,首先会执行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 掉。最终的执行结果为:// 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")
}
这段代码有点刁钻,刚写出来的时候,我自己都没看明白是怎么回事,然后细细品味了一番才悟了!但此时并不会执行7,因为在执行子协程的时候使用到了 job,既然使用到了另一个 job,那么就必须把全局协程执行完才有 job。所以这里会先执行2, 然后遇到延迟1。所以目前的打印顺序为:然后子协程被创建,并不会执行协程体中的代码,因为当前调度前的代码还没有走完还记得 runBlocking 是干什么用的嘛?runBlocking 只会等待自己子协程执行。GlobalScope#launch{} 很显然不是 runBlocking 的子协程。那么子协程肯定是 runBlocking 的子协程吧,是的,没问题。可是这里的 job,是使用的 GlobalScope 的 job。管理协程的生命周期等。runBlocking 都不会等待 GlobalScope 作用域,更不会等待使用 GlobalScope job 的作用域。所以这里的最终结果为:https://gitee.com/lanyangyangzzz/coroutine-project/blob/main/app/src/main/java/com/szj/coroutine/project/jvm/blog3/Client.kt