在协程系列的前两篇稳文章,我们介绍了:
- 协程就是个线程框架
- 协程的挂起本质就是线程切出去再切回来。
什么是「非阻塞式挂起」
非阻塞式是相对阻塞式而言的。
编程语言中的很多概念其实都来源于生活,就像脱口秀的段子一样。
线程阻塞很好理解,现实中的例子就是交通阻塞,它的核心有3点:
- 前面有障碍物,不过不去(线程卡了)
- 需要等障碍物清除后才能过去(耗时任务结束)
- 除非你绕道而行(切到别的线程)
从语义上立即「非阻塞式挂起」,讲的是「非阻塞式」这个是挂起的一个特点,也就是说,协程的挂起,就是非阻塞式,本来就是涉及到多线程。
就像视频里讲的,阻塞不阻塞,都是针对单线程讲的,一旦切了线程,肯定是非阻塞的,你都跑到别的线程了,之前的线程就自由了,可以继续做别的事情了。
所以「非阻塞式挂起」,其实就是在讲协程在挂起的同时切线程这件事情。
为什么要讲非阻塞式挂起
既然第三篇说的「非阻塞式挂起」和第二篇的「挂起到切线程」是同一件事情,那还有讲的必要吗?
是有的。因为它在写法上和单线程的阻塞式是一样的。
协程只是在写法上「看起来阻塞」,其实是「非阻塞」的,因为在协程里面它做了很多工作,其中一个就是帮我们切线程。
第二篇讲挂起,重点是说切线程先切过去,然后再切回来。
第三篇讲非阻塞式,重点是说线程虽然会切,但是写法上和普通的单线程差不多。
让我们来看看下面的例子:
main {
GlobalScope.launch(Dispatchers.Main) {
// 耗时操作
val user = suspendingRequestUser()
updateView(user)
}
private suspend fun suspendingRequestUser(): User = withContext(Dispatchers.IO) {
api.requestUser()
}
}
从上面的例子可以看到,耗时操作和更新UI的逻辑像写单线程一样放在了一起,只是在外面包了一层协程。
而正是这个协程解决了原来我们单线程写法会卡线程这件事。
阻塞的本质
首先,所有的代码本质上都是阻塞的,而只有比较耗时的代码才会导致人类可感知的等待,比如正在主线程上做一个耗时50ms的操作会导致界面卡掉几帧,这种是我们人眼能观察出来的,而这就是我们通常意义所说的「阻塞」。
举个例子,当你开发的app在性能好的手机上很流畅,在性能差的的老手机上会卡顿,就是再说一行代码执行的时间不一样。
视频中讲了一个网络IO的例子,IO阻塞更多是反映在「等」这件事情上,它的性能瓶颈和网络的数据交换,你切多少个线程都没用,该花的时间一点都少不了。
协程与线程
协程我们讲了3期,Kotlin协程与线程是无法脱离开讲的。
别的语言我不说,在Kotlin里,协程就是基于线程来实现的一种更上层的工具API,类似于Java中自带的Executor系列API或者Android的Handler系列API。
只不过呢,协程它不仅提供了方便的API,在设计思想上是一个基于线程的上层框架,你可以理解为创造了一些概念来帮助你更高地是使用这些API,仅此而已。
就像ReactiveX一样,为了让你更好地使用各种操作符API,新造了Observable等概念。
说到这里,Kotlin协程的散发疑问:协程是什么、挂起是什么、挂起的非阻塞式是怎么回事,就已经全部讲完了。非常简单:
- 协程就是切线程
- 挂起就是可以自动切回来的切线程
- 挂起的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作,就这么简单。
当然了,这几句是总结,它们背后的原理你是一定要掌握住的。
Kotlin协程并没有脱离Kotlin或者JVM创造新的东西,它只是将多线程的开发变得更简单了,可以说是因为Kotlin的诞生而顺其自然出现的东西,从语法上看它很神奇,但从原理上讲,它并不是魔术。
练习题 使用协程实现一个网络请求:
- 等待时显示 Loading;
- 请求成功或者出错让 Loading 消失;
- 请求失败需要提示用户请求失败了;
- 让你的协程写法上看上去像单线程。
fun main() {
runBlocking {
launch {
try {
showLoading()
val result = doNektworRequest()
handleSuccess(result)
} catch (e: Exception) {
handleFailure(e)
} finally {
hideLoading()
}
}
}
}
suspend fun showLoading() {
//在此处显示加载状态
println("Loading......")
}
suspend fun doNetworkRequest(): String {
// 模拟网络请求
delay(2000)
// 这里可以使用实际的网络请求逻辑
return "Request Result"
}
suspend fun handleSuccess(result: String) {
//在此处请求成功的逻辑
println("Request succeeded. Result: $result")
}
suspend fun handleFailure(exception: Exception) {
println("Request failed. Exception: ${exception.message}")
}
suspend fun hideLoading() {
// 此处隐藏加载状态
pringln("Loading finished.")
}
在上述代码中,runBlocking创建了一个协程作用域,launch启动了一个新的协程来执行网络请求。在协程中,我们通过的调用相应的挂起函数来模拟请求的不同阶段。showLoading()函数用于显示加载状态,doNetworkRequest()函数模拟网络请求并返回结果。
handleSuccess()和handleFailure()函数分别处理请求成功和失败逻辑,hideLoading()函数用于隐藏加载状态。
这个示例中的协程代码外观看起来像单线程代码,使用了顺序结构,同时满足了要求:在请求间显示加载状态,请求成功或失败后隐藏加载状态,并根据执行结果相应的处理逻辑。
请注意,这只是一个简单的例子。
版权声明
本文首发于:到底什么是「非阻塞式」挂起?协程真的更轻量级吗?
微信公众号:扔物线