🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## 参考文章 [到底什么是「非阻塞式」挂起?协程真的更轻量级吗?](https://kaixue.io/kotlin-coroutines-3/) 在协程系列的前两篇文章中,我们介绍了: * 协程就是个线程框架 * 协程的挂起本质就是线程切出去再切回来 ## 什么是「非阻塞式挂起」 非阻塞式是相对阻塞式而言的。不卡线程就是非阻塞式 编程语言中的很多概念其实都来源于生活,就像脱口秀的段子一样。 线程阻塞很好理解,现实中的例子就是交通堵塞,它的核心有 3 点: * 前面有障碍物,你过不去(线程卡了) * 需要等障碍物清除后才能过去(耗时任务结束) * 除非你绕道而行(切到别的线程) 从语义上理解「非阻塞式挂起」,讲的是**非阻塞式,这个是挂起的一个特点**,也就是说,**协程的挂起,就是非阻塞式的,协程是不讲「阻塞式的挂起」的概念的**。 网络上有一种说法是:协程的挂起是非阻塞式的,而线程时阻塞式的。这种说法是具有严重误导性的。搞得好像协程的异步比线程的异步更高级一样,但其实这里的线程是阻塞式的是指的是单线程是阻塞式的,因为单线程中的耗时代码会卡线程。而单协程也可以是非阻塞式的,因为它可以利用挂起函数来切线程。但实际上Kotlin协程的挂起就是切线程而已。它和Java的切线程是完全一样的,只是在写法上,上下两行连续代码,协程可以悄悄地把线程切走再切回来,不会卡当前线程,这个就是所谓的非阻塞式挂起。而不用协程的话,上下两行连续代码只能是单线程的。那当然会卡线程了。所以,协程的挂起函数跟Java原始的线程切换,其实都是非阻塞式的。只是协程是看起来阻塞,但实际上却非阻塞的写法而已。 我们讲「非阻塞式挂起」,其实它有几个前提:并没有限定在一个线程里说这件事,因为挂起这件事,本来就是涉及到多线程。 就像视频里讲的,阻塞不阻塞,都是针对单线程讲的,一旦切了线程,肯定是非阻塞的,你都跑到别的线程了,之前的线程就自由了,可以继续做别的事情了。 所以「非阻塞式挂起」,其实就是在讲协程在挂起的同时切线程这件事情。 ## 为什么要讲非阻塞式挂起 既然第三篇说的「非阻塞式挂起」和第二篇的「挂起要切线程」是同一件事情,那还有讲的必要吗? 是有的。因为它在写法上和单线程的阻塞式是一样的。 **协程只是在写法上「看起来阻塞」,其实是「非阻塞」的,因为在协程里面它做了很多工作,其中有一个就是帮我们切线程**。 第二篇讲挂起,重点是说切线程先切过去,然后再切回来。 **第三篇讲非阻塞式,重点是说线程虽然会切,但写法上和普通的单线程差不多**。 让我们来看看下面的例子: ~~~kotlin 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 的诞生而顺其自然出现的东西,从语法上看它很神奇,但从原理上讲,它并不是魔术**。 ## 错误的网络观点 ### 错误观点一、协程的非阻塞式比线程的更加高效,**其实这是错误的观点** * 网友解释:如果用线程来处理网络请求,那么在网络请求返回之前,线程会一直等着它,是处于阻塞式状态不做事的,那么这就导致了线程的利用率不高;而如果用协程,由于协程在等待网络请求的过程中会被挂起,线程没有被阻塞,这就提高了线程的利用率。 * 反方(凯哥):听着这种解释好有道理,但这种说法是错误的,首先,所有的代码本质上都是阻塞式的,而只有比较耗时的代码才能导致人类可感知的等待,比如你在主线程做一个几十毫秒的操作,它就会导致你的界面卡掉几帧,这是我们肉眼可以观察到的,而这就是我们通常意义所说的阻塞,而耗时操作,我们之前也讲了,一共分为两种,I/O 操作和 CPU 计算耗时工作,而网络就属于I/O 操作,它的性能瓶颈就是I/O ,也就是和网络的数据交互,而不是CPU的计算速度。所以线程会被网络交互所阻塞,但这个阻塞是不可避免的,你必须做这个I/O,那它比较慢怎么办,不怎么办,没办法,你只能让线程在那里慢慢处理,这里注意,它只是在慢慢地处理,而不是单纯地等待,它等待只是因为网络传输的性能低于CPU的性能,但它本质上是在工作的。这种阻塞不可避免。那协程不是可以挂起么?不用傻傻等待了么,这里你忘了,协程挂起的本质就是切线程,而网络请求时的挂起是什么?还是切线程,它是把主线程给空置出来,然后在后台线程去做网络交互,而不是“先切到后台去做网络请求,然后这个网络请求到达那个所谓的等待阶段再挂起一次。通过这种方式让后台的这个网络交互线程空出来,然后这个网络线程就能去立即做别的网络请求,就不用傻等了”,没有这种好事的,你把网络交互线程给空出来,它就能立即去做下一个网络请求了,好爽是吧,那你刚才那个正在等待的网络请求怎么办?它还是有另外一个线程来承载的呀,不然你觉得这个网络交互会凭空地自己完成?这是不可能的,一定要注意,挂起的本质就是切线程,只是它在完成之后能够自动的切回来,没有其他神奇之处了。所以协程的非阻塞式挂起,只是用阻塞式的方式写出了非阻塞式的代码而已,并没有任何相比于线程更加高效的地方。**在Kotlin里,协程就是基于线程而实现的一套更上层的工具API**。 ### 错误观点二、协程是用户态的,它的切换不需要和操作系统进行交互,因此它的切换成本比线程低。还有人说协程由于是协作式的,所以不需要线程的同步操作。 上面的观点对于有些语言描述是对的,但是对于Kotlin来说完全是无稽之谈,所以有些文章的作者其实是不懂装懂的。一定要记住,**协程的本质还是线程**。 ### 错误观点三、Kotlin官方说Kotlin协程相当于轻量级线程。 官方的示例——[协程很轻量](http://www.kotlincn.net/docs/reference/coroutines/basics.html#%E5%8D%8F%E7%A8%8B%E5%BE%88%E8%BD%BB%E9%87%8F) ``` import kotlinx.coroutines.* fun main() = runBlocking { repeat(100_000) { // 启动大量的协程 launch { delay(1000L) print(".") } } } ``` “同时执行10万个延时任务,用协程没问题,但是用线程的话,多半要内存溢出,所以协程比线程要轻量级。” 这是错误的,这里的比较对象是Java的Thread,而协程的比较对象更接近的应该是Java的线程池API,也就是ExecutorService那几个类。如果是和它们作比较,使用和不使用协程的性能就不相上下了。如下面的代码所示 ~~~ fun main(args: Array<String>) { val executor = Executors.newCachedThreadPool() val task = Runnable { Thread.sleep(1000L) print(".") } repeat(100_000) { executor.execute(task) } } ~~~ 官方狡猾的地方,除了把对比对象直接定为了Thread而不是线程池API之外,还有一点是它这个的延时操作,协程的对比对象使用的是线程的`Thread.sleep()`方法,这样的话,如果用普通的线程池和协程比较,依然会出现协程性能更高的结果,但其实协程的这个延时操作,它对应的应该是Java里面的`Executors.newSingleThreadScheduledExecutor`,而不是上面代码中的 `Executors.newCachedThreadPool`,如下所示,那用不用协程的性能就真的彻底没有区别了,Thread是最底层的控件,而Executor和Coroutine都是基于它所创造出来的工具包。Kotlin官方偷换了概念,把直接使用Thread说成是比协程重,搞得好像协程在性能上真的是有优势一样。 ~~~ fun main(args: Array<String>) { val executor = Executors.newSingleThreadScheduledExecutor() val task = Runnable { print(".") } repeat(100_000) { executor.schedule(task, 1, TimeUnit.SECONDS) } } ~~~ ![](https://img.kancloud.cn/c6/9e/c69ea4d6e770d5b32092e24aa5fd69a3_1862x860.gif)