什么是「非阻塞式挂起」
非阻塞式是相对阻塞式而言的。
编程语言中的很多概念其实都来源于生活,就像脱口秀的段子一样。
线程阻塞很好理解,现实中的例子就是交通堵塞,它的核心有 3 点:
- 前面有障碍物,你过不去(线程卡了)
- 需要等障碍物清除后才能过去(耗时任务结束)
- 除非你绕道而行(切到别的线程)
从语义上理解「非阻塞式挂起」,讲的是「非阻塞式」这个是挂起的一个特点,也就是说,协程的挂起,就是非阻塞式的,协程是不讲「阻塞式的挂起」的概念的。
我们讲「非阻塞式挂起」,其实它有几个前提:并没有限定在一个线程里说这件事,因为挂起这件事,本来就是涉及到多线程。
就像视频里讲的,阻塞不阻塞,都是针对单线程讲的,一旦切了线程,肯定是非阻塞的,你都跑到别的线程了,之前的线程就自由了,可以继续做别的事情了。
所以「非阻塞式挂起」,其实就是在讲协程在挂起的同时切线程这件事情。
为什么要讲非阻塞式挂起
既然第三篇说的「非阻塞式挂起」和第二篇的「挂起要切线程」是同一件事情,那还有讲的必要吗?
是有的。因为它在写法上和单线程的阻塞式是一样的。
协程只是在写法上「看起来阻塞」,其实是「非阻塞」的,因为在协程里面它做了很多工作,其中有一个就是帮我们切线程。
第二篇讲挂起,重点是说切线程先切过去,然后再切回来。
第三篇讲非阻塞式,重点是说线程虽然会切,但写法上和普通的单线程差不多。
让我们来看看下面的例子:
🏝️
main {
GlobalScope.launch(Dispatchers.Main) {
// 👇 耗时操作
val user = suspendingRequestUser()
updateView(user)
}
private suspend fun suspendingRequestUser() : User = withContext(Dispatchers.IO) {
api.requestUser()
}
}
从上面的例子可以看到,耗时操作和更新 UI 的逻辑像写单线程一样放在了一起,只是在外面包了一层协程。
而正是这个协程解决了原来我们单线程写法会卡线程这件事。
阻塞的本质
首先,所有的代码本质上都是阻塞式的,而只有比较耗时的代码才会导致人类可感知的等待,比如在主线程上做一个耗时 50 ms 的操作会导致界面卡掉几帧,这种是我们人眼能观察出来的,而这就是我们通常意义所说的「阻塞」。
举个例子,当你开发的 app 在性能好的手机上很流畅,在性能差的老手机上会卡顿,就是在说同一行代码执行的时间不一样。
视频中讲了一个网络 IO 的例子,IO 阻塞更多是反映在「等」这件事情上,它的性能瓶颈是和网络的数据交换,你切多少个线程都没用,该花的时间一点都少不了。
而这跟协程半毛钱关系没有,切线程解决不了的事情,协程也解决不了。
协程与线程
协程我们讲了 3 期,Kotlin 协程和线程是无法脱离开讲的。
别的语言我不说,在 Kotlin 里,协程就是基于线程来实现的一种更上层的工具 API,类似于 Java 自带的 Executor 系列 API 或者 Android 的 Handler 系列 API。
只不过呢,协程它不仅提供了方便的 API,在设计思想上是一个基于线程的上层框架,你可以理解为新造了一些概念用来帮助你更好地使用这些 API,仅此而已。
就像 ReactiveX 一样,为了让你更好地使用各种操作符 API,新造了 Observable 等概念。
说到这里,Kotlin 协程的三大疑问:协程是什么、挂起是什么、挂起的非阻塞式是怎么回事,就已经全部讲完了。非常简单:
协程就是切线程;
挂起就是可以自动切回来的切线程;
挂起的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作,就这么简单。
当然了,这几句是总结,它们背后的原理你是一定要掌握住的。如果忘了,再去把之前的视频和文章看一遍就好。
视频中还纠正了官方文档里面的一个错误,这里就不再重复了,最后想表达一点:
Kotlin 协程并没有脱离 Kotlin 或者 JVM 创造新的东西,它只是将多线程的开发变得更简单了,可以说是因为 Kotlin 的诞生而顺其自然出现的东西,从语法上看它很神奇,但从原理上讲,它并不是魔术。