线程和协程 带你深刻易懂了解进程
作者 | 肖玮
写在最前
本故事驳回繁复明了的对话形式,尽洪荒之力让你在轻松无累赘的气氛中,稍微深化地理解进程、线程和协程的相关原理常识
假设你觉得自己原本就曾经了解得很透彻了,那也无妨瞧一瞧,指不定有异常的收获呢
在这个 AI 内容生成众多的时代,依然有一批人"傻傻"保持原创,假设您能读到最后,还请点赞或收藏或关注允许下我呗,感谢 ( ̄︶ ̄)↗
进程
丹尼尔:蛋兄,我对进程、线程、协程这些概念似懂非懂的,要不我们当天就好好聊聊这些?
蛋先生:当然可以
丹尼尔:先说说进程吧,从字面意思上看,是不是可以了解为正在运(进)行的程序?
蛋先生:正是如此,程序是静态的,而进程则是灵活的
丹尼尔:说得我更懵懂了
蛋先生:好吧,以你电脑上的视频播放器(就是一个程序)为例。当你不双击它时,它就是一个宁静的美女子——哦不,就是一份静静躺在硬盘上的代码
丹尼尔:别逗我了,蛋兄
蛋先生:( ╯▽╰) 但当你双击它时,它就经过进程“动”起来了
丹尼尔:进程做了什么让它“动”起来了?
蛋先生:程序是代码,比如播放逻辑的代码。要让视频播放,这些代码必需口头起来对吧
丹尼尔:确实。那进程是怎样口头这些代码的?
蛋先生:进程会应用操作系统的调度器调配给它的 CPU 期间片,经过 CPU 来口头代码(留意:现代操作系统都是间接调度线程,不会调度进程哦)
丹尼尔:原来如此,操作系统给进程调配了 CPU 期间片资源。那还有其他的资源吗?
蛋先生:代码口头环节,须要存储一些数据,所以进程还调配有内存空间资源
丹尼尔:都存些什么数据呢?
蛋先生:程序代码自身就须要先存储起来。而后辈码口头环节中的变量,参数什么的,也是须要存储的。给个图你了解一下吧
丹尼尔:哦,还有其它资源吗?
蛋先生:程序或许会口头一些 I/O 义务,比如视频播放器须要加载视频,这些视频数据或许从本地文件加载,也或许从网络上加载,这就须要文件形容符资源。计算,存储,I/O 触及的三大资源,就是调配给进程最关键的资源了。而进程就是调配资源的基本单位了
丹尼尔:原来如此,代码口头,数据存储,I/O 操作,程序就能运转起来了
蛋先生:正是这样。有了进程,我们可以同时运转多个程序。比如,你可以一边播放视频,一边编辑文档,每个程序都有自己的进程,互不搅扰。即使它们都是同一份代码,但各自播放的内容和进展都可以不同
丹尼尔:明确了
蛋先生:既然你有编程基础,我就繁难总结一下吧。
什么是进程?进程就是程序的实例(就像面向对象编程中的类,类是静态的,只要实例化后才运转,且同一个类可以有多个实例)
为什么须要进程?为了让程序运转起来(假设程序不运转,用户昨看视频捏)
线程
丹尼尔:这个总结我青睐,接上去该聊聊线程了
蛋先生:进程(可以看成只要一个线程的进程)同时只能做一件事,所以你的视频播放器的上班形式就像以下
丹尼尔:那样的体验必需蹩脚透了,视频齐全加载并解码完之前,啥都看不了
蛋先生:没错,所以我们希冀能够一边加载和解码,一边播放,这样就不会糜费期间空等了。为了成功这个目标,一个进程就须要退化成多个线程来同时口头多个义务
丹尼尔:那假设一个进程只能做一件事,我用两个进程不也可以同时做两件事吗?
蛋先生:你说得对,但进程间是齐全独立的,互不搅扰。而线程则共享同一个进程的资源,所以线程间替换数据更繁难,简直没有通讯损耗。但进程间替换数据就费事多了,得经过一些通讯机制,比如管道、信息队列之类的
构想一下,我和你住在不同的房子,你要寄给我一箱牛奶,就得经过快递等形式寄给我。但假设我和你住在同一个房子,你买了牛奶只需往冰箱一放,我只需去冰箱一拿,多繁难啊
丹尼尔:那线程都共享进程的什么资源呢?
蛋先生:调配给进程的资源,绝大部分都是线程间共享的。比如内存空间的代码段,数据段,堆,比如文件形容符等。而栈则是每个线程特有的,由于线程是程序口头的最小单位,它须要记载自己的部分变量等
共享资源笼罩
丹尼尔:线程之间共享资源,总觉得会有什么疑问
蛋先生:大部分状况下线程之间还是可以敌对共处的,但有一种状况,就是大家都想对同个资源启动写操作时,就会出现笼罩,造成数据不分歧等疑问
丹尼尔:能详细说一说吗?
蛋先生:为了更容易了解,我们借助以下代码来说明。假设两个线程来运转 main 方法,会有概率出现一些让你隐晦的结果
// 定义一个静态成员变量 a int a // 定义一个方法 add 来参与 a 的值 a Systemout a
丹尼尔:怎样说?
蛋先生:a 是个静态成员变量,它存储在进程内存空间的数据段,共享于多个线程,所以它属于线程间共享的资源对吧
丹尼尔:没错
蛋先生:我们再看下add方法的逻辑a += 1, 这么繁难的代码,在底层并非原子操作,而是分为三个步骤
丹尼尔:那会有什么疑问呢?
蛋先生:假设线程 1 在口头完步骤一和步骤二,还没口头步骤三时,操作系统启动了 CPU 调度,出现了线程切换,使得线程 2 也开局口头步骤一和步骤二。接上去线程 1 和线程 2 都会各自口头步骤三。由于 add 方法口头了两次,正确的结果 a 的值应该是 +2。但很遗憾,结果是 +1。这样的结果有时刻会让你摸不着头脑,而不稳固的结果也将会造成运行的不稳固
丹尼尔:啊,是这样啊。那该怎样办?
蛋先生:处置方法有很多种,比如加锁打算,比如无锁打算等,须要依据实践状况选用。这个话题比拟复杂,我们前面再找期间详细讨论吧。如今只需知道多线程会有资源笼罩的疑问就行了
高低文切换
丹尼尔:好的,明确了。刚才提到线程切换,线程切换究竟出现了什么呢?
蛋先生:线程切换会启动线程高低文切换。线程在运转时,实践上是在口头代码,而口头代码环节中须要存储一些两边数据,也或许会口头一些 I/O 操作。假设环节中被终止,是不是得保管现场,以便下次复原继续运转?
丹尼尔:嗯,确实须要,但详细都存储些什么呢?
蛋先生:首先是下一个要口头的代码,这个存储在程序计数器中。而后是一些两边数据如部分变量等,会存储在线程栈中。为了减速计算,两边数据中对指令口头至关关键的部分会存储在寄存器中。所以,程序计数器须要保管,寄存器须要保管,线程栈指针也须要保管
丹尼尔:“两边数据中对指令口头至关关键的部分会存储在寄存器”,能举个例子吗?
蛋先生:假定以下代码,当在口头 add 方法时,x, y, a, b 会压进线程栈中。而其中 a, b 是和运算最相关的,则会存储在寄存器中,以减速 CPU 的运算
int a bint int x int y int result x y
协程
丹尼尔:哦,原来如此。线程曾经相当不错了,那协程又是怎样回事呢?
蛋先生:回顾一下,我们之前一个线程担任运转加载和解码逻辑,另一个线程担任播放逻辑,对吧?
丹尼尔:没错,有什么疑问吗?
蛋先生:其实还有提升的空间。线程在口头加载视频片段时,必需等候结果前往能力口头解码操作
丹尼尔:确实,加载片段的等候期间仿佛又被糜费了
蛋先生:没错,我们可以充沛应用这段期间。只需让线程在加载的同时启动解码,就能大幅缩小加载等候的期间。而这正是协程所能施展的作用
丹尼尔:哇,蛋兄,你可真是个会过日子的人,这么一丝不苟。但我只需用不同的线程区分处置加载和解码,不也能到达雷同的成果吗?
蛋先生:可以是可以,但多线程会带来一些疑问
丹尼尔:啥疑问呢?
蛋先生:首先,一个线程用于口头加载操作,这关键是 I/O 操作,简直不消耗 CPU 资源,造成该线程长期间处于阻塞形态,这是很糜费的。当然,你可以让它休眠以监禁 CPU 期间,但创立线程自身就有开支,线程切换雷同有开支。相比之下,协程十分轻量,创立和切换的开支极小
丹尼尔:为什么协程的创立和切换的开支极小呢?
蛋先生:关键是由于它并非操作系统层面的物品,就不触及内核调度。普通是由编程言语来成功(比如 Python 的 asyncio 规范库),它属于用户态的物品
丹尼尔:那协程不会有像多线程那样的资源笼罩疑问吗?
蛋先生:线程的口头机遇由操作系统调度,程序员不可控制,这正是多线程容易出现资源笼罩的关键要素。而协程的口头机遇由程序自身控制,不受操作系统调度影响,因此可以齐全防止这类疑问
此外,同一个线程内的多个协程共享同一个线程的 CPU 期间片资源,它们在 CPU 上的口头是有先后顺序的,不能并行口头。而线程是可以并行口头的
丹尼尔:那协程是如何成功这一点的呢?
蛋先生:协程(coroutine),其实是一种不凡的子程序(subroutine,比如普通函数)。普通函数一旦口头就会从头到尾运转,而后前往结果,两边不会暂停。而协程则可以在口头到一半时暂停。应用这一个性,我们可以在遇到 I/O 这类不消耗 CPU 资源的操作时,将其挂起,继续口头其他计算义务,充沛应用 CPU 资源。等 I/O 操作结果前往时,再复原口头
丹尼尔:觉得很像 NodeJS 的异步 I/O 啊
蛋先生:没错,它们的目标都是在一个线程内并发口头多个义务。不过在叫法和成功上会有一些差异
丹尼尔:觉得当天了解得够多了,谢谢蛋兄
蛋先生:后会有期!