进程、线程和协程概念性的东西这里就不会讲很多,一句话概括:进程、线程和协程实际上都是为并发而生。
但是它们的内存模型是不一样的,下面我们来分析一下各自的特点和关系。
进程及内存模型
进程,是程序的基本执行实体,即可执行程序运行中形成一个独立的内存体,这个内存体有自己独立的地址空间,有独立的堆空间。操作系统会以进程为单位,分配系统资源(CPU 时间片、内存等资源),也就是说进程是系统资源分配的最小单位,如下图所示:
进程特性
- 动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的
- 并发性:任何进程都可以同其他进程一起并发执行
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
- 异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
- 结构特征:进程由程序、数据和进程控制块三部分组成。
多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果。
进程状态转换如下所示:
线程及内存模型
线程,有时被称为轻量级进程,是操作系统调度(CPU 调度)执行的最小单位,如下图所示。
线程特性
在多线程 OS 中,通常是在一个进程中包括多个线程,每个线程都是作为利用 CPU 的基本单位,是花费最小开销的实体。线程具有以下属性。
- 轻型实体:线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。线程的实体包括程序、数据和 TCB(线程控制块)
- 独立调度和分派的基本单位:在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位
- 可并发执行:在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行
- 共享进程资源:在同一进程中的各个线程,都可以共享该进程所拥有的资源
Linux 最小执行单元 CPU
对于 Linux 来讲,它不区分你是进程还是线程的,他们都是一个单独的执行单位,CPU 均是通过调度分配时间片来执行,但是对于多核来说是有一定优势的。
由上图可知,如果一个进程想更大程度的与其他进程抢占 CPU 的资源,那么可以通过开多个线程的方式来达到。
可以看到进程 A 默认开了 1 个线程,对于内核来讲,它只有 1 个执行单元,进程 B 开了 3 个线程,那么在内核中,该进程就占有 3 个执行单元。
CPU 在内核空间的,它不区分是进程和还是线程,而是通过时间片轮询的方式平均调度分配。因此,进程 B 开了 3 个单元就相对占了比较多的资源。
线程上下文切换
由上述可知,在一定条件下,线程越多,进程利用或者说抢占的 cpu 资源就会越高,如下图所示:
那么是不是线程可以无限制的开启呢?
答案当然不是的,我们知道,当我们 cpu 在内核态切换一个执行单元的时候,会有一个上下文切换的时间成本和性能开销,如下图:
其中性能开销至少有以下两个方面:
- 切换内核上下文栈
- 保存寄存器中的状态内容
因此,我们不能大量的开辟线程。很多编程语言在设计上就想了办法,既然我们不能优化 cpu 切换线程的开销,那么我们是否可以在用户态优化该流程呢?
很显然,我们是没权限修改操作系统内核机制的,那么只能在用户态再来一个伪执行单元,那么就是协程了。
协程及切换成本
协程切换比线程切换快主要有两点:
(1)协程切换完全在用户空间,而进行线程切换涉及特权模式切换,需要在内核空间完成;
(2)协程切换相比线程切换做的事情更少,线程需要有内核和用户态的切换,涉及系统调用过程。
协程切换成本
协程切换比较简单,就是把当前协程的 CPU 寄存器状态保存起来,然后将需要切换进来的协程的 CPU 寄存器状态加载的 CPU 寄存器上就 ok 了。而且完全在用户态进行,一般来说一次协程上下文切换最多就是几十 ns 这个量级。
线程切换成本
系统内核调度的对象是线程,因为线程是调度的基本单元(进程是资源拥有的基本单元,进程的切换需要做的事情更多,这里占时不讨论进程切换),而线程的调度只有拥有最高权限的内核空间才可以完成,所以线程的切换涉及到用户空间和内核空间的切换,也就是特权模式切换。
进程一般占用 1 ~ 4g 不等的内存,G 量级。 线程一般占用 2 ~ 8M 不等内存,MB 量级。 而协程占用 2 ~ 4KB 内存,KB 量级。
那么,go 的协程切换成本如此小,占用也那么小,是否可以无限开辟呢? 前面有一篇文章讲到过这个问题,不妨回顾下:Golang无限开启Goroutine?该如何限定Goroutine数量?
总结
总的来说,进程、线程和协程切换都需要性能开销,只是协程开销可以忽略,而且它是用户态的,可以有用户程序来控制,优势比较明显,尤其是Go语言的协程设计。