带你通俗易懂了解进程、线程和协程

带你通俗易懂了解进程、线程和协程

文章图片

带你通俗易懂了解进程、线程和协程

文章图片

带你通俗易懂了解进程、线程和协程

文章图片

带你通俗易懂了解进程、线程和协程

文章图片

作者:肖玮

本故事采用简洁明了的对话方式 , 尽洪荒之力让你在轻松无负担的氛围中 , 稍微深入地理解进程、线程和协程的相关原理知识

写在最前本故事采用简洁明了的对话方式 , 尽洪荒之力让你在轻松无负担的氛围中 , 稍微深入地理解进程、线程和协程的相关原理知识
如果你觉得自己本来就已经理解得很透彻了 , 那也不妨瞧一瞧 , 指不定有意外的收获呢
在这个 AI 内容生成泛滥的时代 , 依然有一批人\"傻傻\"坚持原创 , 如果您能读到最后 , 还请点赞或收藏或关注支持下我呗 , 感谢 ( ̄︶ ̄)↗
进程丹尼尔:蛋兄 , 我对进程、线程、协程这些概念似懂非懂的 , 要不咱们今天就好好聊聊这些?
蛋先生:当然可以
丹尼尔:先说说进程吧 , 从字面意思上看 , 是不是可以理解为正在运(进)行的程序?
蛋先生:正是如此 , 程序是静态的 , 而进程则是动态的
丹尼尔:说得我更糊涂了
蛋先生:好吧 , 以你电脑上的视频播放器(就是一个程序)为例 。 当你不双击它时 , 它就是一个安静的美男子——哦不 , 就是一份静静躺在硬盘上的代码
丹尼尔:别逗我了 , 蛋兄
蛋先生:( ╯▽╰) 但当你双击它时 , 它就通过进程“动”起来了
丹尼尔:进程做了什么让它“动”起来了?
蛋先生:程序是代码 , 比如播放逻辑的代码 。 要让视频播放 , 这些代码必须执行起来对吧
丹尼尔:确实 。 那进程是怎么执行这些代码的?
蛋先生:进程会利用操作系统的调度器分配给它的 CPU 时间片 , 通过 CPU 来执行代码(注意:现代操作系统都是直接调度线程 , 不会调度进程哦)
丹尼尔:原来如此 , 操作系统给进程分配了 CPU 时间片资源 。 那还有其他的资源吗?
蛋先生:代码执行过程 , 需要存储一些数据 , 所以进程还分配有内存空间资源
丹尼尔:都存些什么数据呢?
蛋先生:程序代码本身就需要先存储起来 。 然后代码执行过程中的变量 , 参数什么的 , 也是需要存储的 。 给个图你了解一下吧

丹尼尔:哦 , 还有其它资源吗?
蛋先生:程序可能会执行一些 I/O 任务 , 比如视频播放器需要加载视频 , 这些视频数据可能从本地文件加载 , 也可能从网络上加载 , 这就需要文件描述符资源 。 计算 , 存储 , I/O 涉及的三大资源 , 就是分配给进程最主要的资源了 。 而进程就是分配资源的基本单位了
丹尼尔:原来如此 , 代码执行 , 数据存储 , I/O 操作 , 程序就能运行起来了
蛋先生:正是这样 。 有了进程 , 我们可以同时运行多个程序 。 比如 , 你可以一边播放视频 , 一边编辑文档 , 每个程序都有自己的进程 , 互不干扰 。 即使它们都是同一份代码 , 但各自播放的内容和进度都可以不同
丹尼尔:明白了
蛋先生:既然你有编程基础 , 我就简单总结一下吧 。
什么是进程?进程就是程序的实例(就像面向对象编程中的类 , 类是静态的 , 只有实例化后才运行 , 且同一个类可以有多个实例)
为什么需要进程?为了让程序运行起来(如果程序不运行 , 用户昨看视频捏)
线程丹尼尔:这个总结我喜欢 , 接下来该聊聊线程了
蛋先生:进程(可以看成只有一个线程的进程)同时只能做一件事 , 所以你的视频播放器的工作方式就像以下

丹尼尔:那样的体验肯定糟糕透了 , 视频完全加载并解码完之前 , 啥都看不了
蛋先生:没错 , 所以我们期望能够一边加载和解码 , 一边播放 , 这样就不会浪费时间空等了 。 为了实现这个目的 , 一个进程就需要进化成多个线程来同时执行多个任务

【带你通俗易懂了解进程、线程和协程】丹尼尔:那如果一个进程只能做一件事 , 我用两个进程不也可以同时做两件事吗?

蛋先生:你说得对 , 但进程间是完全独立的 , 互不干扰 。 而线程则共享同一个进程的资源 , 所以线程间交换数据更方便 , 几乎没有通讯损耗 。 但进程间交换数据就麻烦多了 , 得通过一些通讯机制 , 比如管道、消息队列之类的
想象一下 , 我和你住在不同的房子 , 你要寄给我一箱牛奶 , 就得通过快递等方式寄给我 。 但如果我和你住在同一个房子 , 你买了牛奶只要往冰箱一放 , 我只要去冰箱一拿 , 多方便啊

丹尼尔:那线程都共享进程的什么资源呢?
蛋先生:分配给进程的资源 , 绝大部分都是线程间共享的 。 比如内存空间的代码段 , 数据段 , 堆 , 比如文件描述符等 。 而栈则是每个线程特有的 , 因为线程是程序执行的最小单位 , 它需要记录自己的局部变量等
共享资源覆盖丹尼尔:线程之间共享资源 , 总感觉会有什么问题
蛋先生:大部分情况下线程之间还是可以和平共处的 , 但有一种情况 , 就是大家都想对同个资源进行写操作时 , 就会发生覆盖 , 导致数据不一致等问题
丹尼尔:能具体说一说吗?
蛋先生:为了更容易理解 , 我们借助以下代码来说明 。 如果两个线程来运行 main 方法 , 会有概率出现一些让你费解的结果
public class Main {// 定义一个静态成员变量 aprivate static int a = 1;// 定义一个方法 add 来增加 a 的值public static void add() {a += 1;public static void main(String[
args) {add();System.out.println(\"a 的值是: \" + a); // 输出 a 的值
丹尼尔:怎么说?
蛋先生:a 是个静态成员变量 , 它存储在进程内存空间的数据段 , 共享于多个线程 , 所以它属于线程间共享的资源对吧
丹尼尔:没错
蛋先生:我们再看下 add 方法的逻辑 a += 1 ,这么简单的代码 , 在底层并非原子操作 , 而是分为三个步骤
步骤一:获取 a 变量的值
步骤二:执行 +1 运算
步骤三:将运行结果赋值给 a
丹尼尔:那会有什么问题呢?
蛋先生:如果线程 1 在执行完步骤一和步骤二 , 还没执行步骤三时 , 操作系统进行了 CPU 调度 , 发生了线程切换 , 使得线程 2 也开始执行步骤一和步骤二 。 接下来线程 1 和线程 2 都会各自执行步骤三 。 因为 add 方法执行了两次 , 正确的结果 a 的值应该是 +2 。 但很遗憾 , 结果是 +1 。 这样的结果有时候会让你摸不着头脑 , 而不稳定的结果也将会导致应用的不稳定
丹尼尔:啊 , 是这样啊 。 那该怎么办?
蛋先生:解决方法有很多种 , 比如加锁方案 , 比如无锁方案等 , 需要根据实际情况选择 。 这个话题比较复杂 , 我们后面再找时间详细探讨吧 。 现在只要知道多线程会有资源覆盖的问题就行了
上下文切换丹尼尔:好的 , 明白了 。 刚才提到线程切换 , 线程切换到底发生了什么呢?
蛋先生:线程切换会进行线程上下文切换 。 线程在运行时 , 实际上是在执行代码 , 而执行代码过程中需要存储一些中间数据 , 也可能会执行一些 I/O 操作 。 如果过程中被中断 , 是不是得保留现场 , 以便下次恢复继续运行?
丹尼尔:嗯 , 确实需要 , 但具体都存储些什么呢?
蛋先生:首先是下一个要执行的代码 , 这个存储在程序计数器中 。 然后是一些中间数据如局部变量等 , 会存储在线程栈中 。 为了加速计算 , 中间数据中对当前指令执行至关重要的部分会存储在寄存器中 。 所以 , 程序计数器需要保存 , 寄存器需要保存 , 线程栈指针也需要保存
丹尼尔:“中间数据中对当前指令执行至关重要的部分会存储在寄存器” , 能举个例子吗?
蛋先生:假设以下代码 , 当在执行 add 方法时 , x y a b 会压进线程栈中 。 而其中 a b 是和当前运算最相关的 , 则会存储在寄存器中 , 以加速 CPU 的运算
int add(int a int b) {return a + b;int main() {int x = 10;int y = 20;int result = add(x y);return 0协程丹尼尔:哦 , 原来如此 。 线程已经相当不错了 , 那协程又是怎么回事呢?
蛋先生:回想一下 , 我们之前一个线程负责运行加载和解码逻辑 , 另一个线程负责播放逻辑 , 对吧?
丹尼尔:没错 , 有什么问题吗?
蛋先生:其实还有优化的空间 。 线程在执行加载视频片段时 , 必须等待结果返回才能执行解码操作

丹尼尔:确实 , 加载片段的等待时间似乎又被浪费了
蛋先生:没错 , 我们可以充分利用这段时间 。 只需让线程在加载的同时进行解码 , 就能大幅减少加载等待的时间 。 而这正是协程所能发挥的作用

丹尼尔:哇 , 蛋兄 , 你可真是个会过日子的人 , 这么精打细算 。 但我只要用不同的线程分别处理加载和解码 , 不也能达到同样的效果吗?
蛋先生:可以是可以 , 但多线程会带来一些问题
丹尼尔:啥问题呢?
蛋先生:首先 , 一个线程用于执行加载操作 , 这主要是 I/O 操作 , 几乎不消耗 CPU 资源 , 导致该线程长时间处于阻塞状态 , 这是很浪费的 。 当然 , 你可以让它休眠以释放 CPU 时间 , 但创建线程本身就有开销 , 线程切换同样有开销 。 相比之下 , 协程非常轻量 , 创建和切换的开销极小
丹尼尔:为什么协程的创建和切换的开销极小呢?
蛋先生:主要是因为它并非操作系统层面的东西 , 就不涉及内核调度 。 一般是由编程语言来实现(比如 Python 的 asyncio 标准库) , 它属于用户态的东西
丹尼尔:那协程不会有像多线程那样的资源覆盖问题吗?
蛋先生:线程的执行时机由操作系统调度 , 程序员无法控制 , 这正是多线程容易出现资源覆盖的主要原因 。 而协程的执行时机由程序自身控制 , 不受操作系统调度影响 , 因此可以完全避免这类问题
此外 , 同一个线程内的多个协程共享同一个线程的 CPU 时间片资源 , 它们在 CPU 上的执行是有先后顺序的 , 不能并行执行 。 而线程是可以并行执行的
丹尼尔:那协程是如何实现这一点的呢?
蛋先生:协程(coroutine) , 其实是一种特殊的子程序(subroutine , 比如普通函数) 。 普通函数一旦执行就会从头到尾运行 , 然后返回结果 , 中间不会暂停 。 而协程则可以在执行到一半时暂停 。 利用这一特性 , 我们可以在遇到 I/O 这类不消耗 CPU 资源的操作时 , 将其挂起 , 继续执行其他计算任务 , 充分利用 CPU 资源 。 等 I/O 操作结果返回时 , 再恢复执行
丹尼尔:感觉很像 NodeJS 的异步 I/O 啊
蛋先生:没错 , 它们的目的都是在一个线程内并发执行多个任务 。 不过在叫法和实现上会有一些差异
丹尼尔:感觉今天了解得够多了 , 谢谢蛋兄
蛋先生:后会有期!

    推荐阅读