## 参考文章
[到底什么是「非阻塞式」挂起?协程真的更轻量级吗?](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)
- 前言
- Kotlin简介
- IntelliJ IDEA技巧总结
- idea设置类注释和方法注释模板
- 像Android Studion一样创建工程
- Gradle
- Gradle入门
- Gradle进阶
- 使用Gradle创建一个Kotlin工程
- 环境搭建
- Androidstudio平台搭建
- Eclipse的Kotlin环境配置
- 使用IntelliJ IDEA
- Kotlin学习路线
- Kotlin官方中文版文档教程
- 概述
- kotlin用于服务器端开发
- kotlin用于Android开发
- kotlin用于JavaScript开发
- kotlin用于原生开发
- Kotlin 用于数据科学
- 协程
- 多平台
- 新特性
- 1.1的新特性
- 1.2的新特性
- 1.3的新特性
- 开始
- 基本语法
- 习惯用法
- 编码规范
- 基础
- 基本类型
- 包与导入
- 控制流
- 返回与跳转
- 类与对象
- 类与继承
- 属性与字段
- 接口
- 可见性修饰符
- 扩展
- 数据类
- 密封类
- 泛型
- 嵌套类
- 枚举类
- 对象
- 类型别名
- 内嵌类
- 委托
- 委托属性
- 函数与Lambda表达式
- 函数
- Lambda表达式
- 内联函数
- 集合
- 集合概述
- 构造集合
- 迭代器
- 区间与数列
- 序列
- 操作概述
- 转换
- 过滤
- 加减操作符
- 分组
- 取集合的一部分
- 取单个元素
- 排序
- 聚合操作
- 集合写操作
- List相关操作
- Set相关操作
- Map相关操作
- 多平台程序设计
- 平台相关声明
- 以Gradle创建
- 更多语言结构
- 解构声明
- 类型检测与转换
- This表达式
- 相等性
- 操作符重载
- 空安全
- 异常
- 注解
- 反射
- 作用域函数
- 类型安全的构造器
- Opt-in Requirements
- 核心库
- 标准库
- kotlin.test
- 参考
- 关键字与操作符
- 语法
- 编码风格约定
- Java互操作
- Kotlin中调用Java
- Java中调用Kotlin
- JavaScript
- 动态类型
- kotlin中调用JavaScript
- JavaScript中调用kotlin
- JavaScript模块
- JavaScript反射
- JavaScript DCE
- 原生
- 并发
- 不可变性
- kotlin库
- 平台库
- 与C语言互操作
- 与Object-C及Swift互操作
- CocoaPods集成
- Gradle插件
- 调试
- FAQ
- 协程
- 协程指南
- 基础
- 取消与超时
- 组合挂起函数
- 协程上下文与调度器
- 异步流
- 通道
- 异常处理与监督
- 共享的可变状态与并发
- Select表达式(实验性)
- 工具
- 编写kotlin代码文档
- 使用Kapt
- 使用Gradle
- 使用Maven
- 使用Ant
- Kotlin与OSGI
- 编译器插件
- 编码规范
- 演进
- kotlin语言演进
- 不同组件的稳定性
- kotlin1.3的兼容性指南
- 常见问题
- FAQ
- 与Java比较
- 与Scala比较(官方已删除)
- Google开发者官网简介
- Kotlin and Android
- Get Started with Kotlin on Android
- Kotlin on Android FAQ
- Android KTX
- Resources to Learn Kotlin
- Kotlin样品
- Kotlin零基础到进阶
- 第一阶段兴趣入门
- kotlin简介和学习方法
- 数据类型和类型系统
- 入门
- 分类
- val和var
- 二进制基础
- 基础
- 基本语法
- 包
- 示例
- 编码规范
- 代码注释
- 异常
- 根类型“Any”
- Any? 可空类型
- 可空性的实现原理
- kotlin.Unit类型
- kotlin.Nothing类型
- 基本数据类型
- 数值类型
- 布尔类型
- 字符型
- 位运算符
- 变量和常量
- 语法和运算符
- 关键字
- 硬关键字
- 软关键字
- 修饰符关键字
- 特殊标识符
- 操作符和特殊符号
- 算术运算符
- 赋值运算符
- 比较运算符
- 逻辑运算符
- this关键字
- super关键字
- 操作符重载
- 一元操作符
- 二元操作符
- 字符串
- 字符串介绍和属性
- 字符串常见方法操作
- 字符串模板
- 数组
- 数组介绍创建及遍历
- 数组常见方法和属性
- 数组变化以及下标越界问题
- 原生数组类型
- 区间
- 正向区间
- 逆向区间
- 步长
- 类型检测与类型转换
- is、!is、as、as-运算符
- 空安全
- 可空类型变量
- 安全调用符
- 非空断言
- Elvis操作符
- 可空性深入
- 可空性和Java
- 函数
- 函数式编程概述
- OOP和FOP
- 函数式编程基本特性
- 组合与范畴
- 在Kotlin中使用函数式编程
- 函数入门
- 函数作用域
- 函数加强
- 命名参数
- 默认参数
- 可变参数
- 表达式函数体
- 顶层、嵌套、中缀函数
- 尾递归函数优化
- 函数重载
- 控制流
- if表达式
- when表达式
- for循环
- while循环
- 循环中的 Break 与 continue
- return返回
- 标签处返回
- 集合
- list集合
- list集合介绍和操作
- list常见方法和属性
- list集合变化和下标越界
- set集合
- set集合介绍和常见操作
- set集合常见方法和属性
- set集合变换和下标越界
- map集合
- map集合介绍和常见操作
- map集合常见方法和属性
- map集合变换
- 集合的函数式API
- map函数
- filter函数
- “ all ”“ any ”“ count ”和“ find ”:对集合应用判断式
- 别样的求和方式:sumBy、sum、fold、reduce
- 根据人的性别进行分组:groupBy
- 扁平化——处理嵌套集合:flatMap、flatten
- 惰性集合操作:序列
- 区间、数组、集合之间转换
- 面向对象
- 面向对象-封装
- 类的创建及属性方法访问
- 类属性和字段
- 构造器
- 嵌套类(内部类)
- 枚举类
- 枚举类遍历&枚举常量常用属性
- 数据类
- 密封类
- 印章类(密封类)
- 面向对象-继承
- 类的继承
- 面向对象-多态
- 抽象类
- 接口
- 接口和抽象类的区别
- 面向对象-深入
- 扩展
- 扩展:为别的类添加方法、属性
- Android中的扩展应用
- 优化Snackbar
- 用扩展函数封装Utils
- 解决烦人的findViewById
- 扩展不是万能的
- 调度方式对扩展函数的影响
- 被滥用的扩展函数
- 委托
- 委托类
- 委托属性
- Kotlin5大内置委托
- Kotlin-Object关键字
- 单例模式
- 匿名类对象
- 伴生对象
- 作用域函数
- let函数
- run函数
- with函数
- apply函数
- also函数
- 标准库函数
- takeIf 与 takeUnless
- 第二阶段重点深入
- Lambda编程
- Lambda成员引用高阶函数
- 高阶函数
- 内联函数
- 泛型
- 泛型的分类
- 泛型约束
- 子类和子类型
- 协变与逆变
- 泛型擦除与实化类型
- 泛型类型参数
- 泛型的背后:类型擦除
- Java为什么无法声明一个泛型数组
- 向后兼容的罪
- 类型擦除的矛盾
- 使用内联函数获取泛型
- 打破泛型不变
- 一个支持协变的List
- 一个支持逆变的Comparator
- 协变和逆变
- 第三阶段难点突破
- 注解和反射
- 声明并应用注解
- DSL
- 协程
- 协程简介
- 协程的基本操作
- 协程取消
- 管道
- 慕课霍丙乾协程笔记
- Kotlin与Java互操作
- 在Kotlin中调用Java
- 在Java中调用Kotlin
- Kotlin与Java中的操作对比
- 第四阶段专题练习
- 朱凯Kotlin知识点总结
- Kotlin 基础
- Kotlin 的变量、函数和类型
- Kotlin 里那些「不是那么写的」
- Kotlin 里那些「更方便的」
- Kotlin 进阶
- Kotlin 的泛型
- Kotlin 的高阶函数、匿名函数和 Lambda 表达式
- Kotlin协程
- 初识
- 进阶
- 深入
- Kotlin 扩展
- 会写「18.dp」只是个入门——Kotlin 的扩展函数和扩展属性(Extension Functions / Properties)
- Kotlin实战-开发Android