导读
一直在谈论进程,线程,并发编程,异步编程的概念,一直没有从一个通俗的角度去理解过,结合自己这么多年的编程经验,回过头来重新思考一下,进程是什么,线程是什么,协程又是什么?
其实大家看很多书籍和资料,都会看到这样一个结论:进程是操作系统资源分配的最小单位,线程是操作系统调度的最小单位。嗯...,可能很多人看到这句话觉得自己理解了,但是不一定真正理解到这句话的精髓。
引言
进程
进程(process)这个单词在维基的释义如下:
In computing, a process is the instance of a computer program that is being executed by one or many threads.
这句话的解释非常不错,很直白的阐述了两个东西,第一点,进程是一个计算机程序(比如,可执行文件)的实例,其次,一个进程由多个线程组成。
其实更简化的说,process
就是一个 running program。你通过构建你的源代码形成一个可执行文件,这是得到了一个program
,当你在shell
上执行这个program
,它被操作系统加载,为其分配各种资源,操作系统就多了一个你自定义的process
。
其次,进程是有自己独立运行的资源(包括内存,CPU,文件等)的,而且进程间的资源是独立,相互之间不受影响的,这个没有太多为什么,这是操作系统设计的一个准则,从经验上来看,这是一个很好的设计。所以,这就是为什么说“进程是操作系统资源分配的最小单位”。
总的来说,当一个程序被操作系统加载到内存中执行后,它就变成了一个进程,进程不仅仅是运行的程序本身,还报错了操作系统为它分配的资源和运行环境。
线程
那出现了进程的情况下,为什么还需要线程,不是只需要进程就ok了吗。嗯...,这是为什么呢?
这可能涉及到多方面的原因,我们知道操作系统为不同进程分配了不同独立的资源。操作系统的现代设计都是多任务的操作系统,必然存在不同之间的进程间的执行切换,这个切换开销是很大的(比如需要重新完成虚拟内存到物理内存的映射,并清空为上一个进程的存储映射关系的TLB缓存)。为了避免或者尽可能的减少这些开销,我们需要更加一个轻量的并发任务模型,所以提出了线程这个概念。
线程是将进程做了一个更小的划分,从而让操作系统有了更精细化的调度方式。一个进程内的多个线程共享进程的大部分资源,线程间的切换更加轻量,这也极大的满足了我们的并发编程模型的设计。一个进程由多个线程组成,线程是操作系统真正调度的一个最小单位,所以我们说线程是操作系统调度的最小单位。
协程
有些时候,使用线程作为任务调度的基本单位并不都是理想的,虽然线程之间的上下文切换和进程比起来轻量很多,但是面对高并发的场景,我们发现一味的提高线程数数量不仅不能带来性能的收益,反而性能还发生了劣化。这是因为线程数量的增加导致操作系统执行任务切换的开销也增加了,另外,操作系统的资源是有限的,即使没有线程切换的开销,你也不可能无限制的分配任意数量的线程。更值得一提的是,针对IO密集的场景,线程往往是在等待IO就绪,等待的线程数越多,CPU的利用率也就越低,我们肯定还是希望CPU能做更多的事情,而不是空闲在那里。
所以,协程是一种更加轻量的"用户级线程”设计,本质上是为了尽可能减少内核态线程切换的开销,且为了在IO密集的场景下,尽可能的提高CPU利用率的一种并发编程模型范式。现在,我们已经能在很多语言里面看到协程的影子(Rust里面的async
, GO里面的goroutine
,C++ 20里面的Coroutine
)。
场景化示例
当你在电脑上打开一个浏览器,输入网址访问网页时,我们看看发生了啥:
- 首先是进程
浏览器会作为一个独立的进程运行,它拥有自己独立的内存、文件句柄和系统资源。这样即使浏览器崩溃了,也不会影响到旁边正在播放音乐的播放器进程。进程就像一栋独立的房子,互不干扰。 - 接下来是线程
在浏览器进程内部,还有多个线程在同时工作:一个负责界面渲染,一个负责网络请求,一个负责 JavaScript 执行。线程共享同一个进程的资源,但又能各自并行,就像同一栋房子里的不同房间。 - 最后是协程
当浏览器需要同时下载几十张图片、执行异步请求时,如果都用线程来处理,开销太大。于是运行时会使用协程:协程不依赖操作系统调度,而是在用户态由程序员或运行时来切换,切换几乎没有成本,非常适合处理这种高并发的 I/O 请求。就好比在一个房间里,几个人有序地轮流用同一张桌子,不需要频繁换房间。
理解上下文切换
上下文切换是操作系统进行任务调度时,进行任务切换前的一个非常重要的行为,无论是在单核系统还是多核的CPU上,这都是一个非常重要的设计。上下文切换是什么?简单理解,就是操作系统在切换到新的要执行的任务前,对当前正在执行的任务的现场(例如通用寄存器,程序计数器,RSP,RBP等)生成一个快照保存到内存中,以便于下次恢复它的执行时,程序跟什么都没有发生过一样,能够继续正常执行。
上下文切换有针对于进程级别,也有针对于线程级别的,这取决于下一个抢占CPU时间的线程和上一个让出CPU时间的的线程是否是共进程的,如果不是,那么这次切换也会引起进程的切换。