进程、线程以及协程的区别
一、并发和并行
1.1 并发
在操作系统中,一个时间段内有几个程序都处于正在运行的状态,而且这几个程序都是在同一个处理机上运行,但任意一个时刻其实只有一个程序在处理机上运行。
在一个只有单核(单 CPU)的处理器的操作系统中,同一时刻只能有一个进程运行。
假设只有一个进程运行,为了执行多任务,需要将 CPU的时间资源分为很多个时间片,将每个时间片分给一个线程,每个线程就可以执行不同的任务。
这样做的好处是,每个线程只占用一个时间片,当一个任务阻塞,当耗尽了内核分配给它的一个时间片后就会挂起,接着执行其他任务,后续再切换到阻塞的线程时也只能占用一个时间片的时间。
有些文章说,内核将时间片分给进程,其实不算准确,因为线程才是程序实际运行时的单元,进程只是一个容器,最少包含一个线程。当多个程序运行时,内核表面上是会将时间片分配给进程,但实际上是根据进程里的线程数分配时间的。
1.2 并行
现在的市面上已经没有单核处理器了,最低端的处理器也是多核(多 CPU)。与单核同样,每个核心同一时刻也只能运行一个进程。
同样假设每个核心只有一个进程,如果每个进程上都只运行一个程序(只开一个线程),这些程序因为是运行在不同的核心上,占用的不是同一个 CPU 资源,所以可以在同一时刻运行,且互不干扰,这就是并行。
1.3 二者的区别
并发和并行的区别就在于同时二字。
虽然并发和并行都能运行多个程序,但区别就在于:
- 并发是多个程序交替运行,因为时间片很短,用户并不会感觉到
- 时间片的分配标准也是以可感知程度设计的,Linux 的时间片范围为 5ms ~ 800ms
- 并行是多个程序同时运行
二、进程、线程和协程在内存上的区别
2.1 进程内存
进程是系统进行资源分配的最小单位,是操作系统结构的基础。
在进程中,运行的程序中会产生一个独立的内存体,这个内存体内有自己独立的内存空间,有自己的堆,上级挂靠的是操作系统。
操作系统会以进程为单位分配系统资源(CPU 时间片、内存等资源)。
进程的内存占用在 32 位系统中为 4G,64 位系统可以达到 T 级。
2.2 线程内存
线程是系统能够运行运算调度的最小单位。
一条线程是进程中的一个单一顺序的控制流,一个进程中可以并发多个线程,每个线程在宏观上并行(微观串行)执行不同的任务。
同一进程中的多条线程共享该进程中的全部系统资源,如虚拟地址空间、文件描述符等。但每一个线程都有各自的调用栈、独立的寄存器环境、线程本地存储。
2.3 协程内存
协程,又被称为微线程,顾名思义,就是轻量级的线程。
在协程初始化创建的时候为其分配的栈为2kB(不同语言的协程的栈内存可能不同,同一语言的不同版本也可能不同,此处以 Go 1.4+ 的协程为例),而线程栈要比这个数字大得多,Linux系统上可以通过ulimit -s
命令来查看线程内存占用。
$ ulimit -s
8192 Kb
在高并发web服务器中,如果为每个请求创建一个协程去处理,100万并发只需要2G内存,如果使用线程,需要的内存高达数 T。
2.3.1 粗略计算内存占用
下面使用使用 Go 代码简单计算一下协程的内存占用:
package main
import (
"time"
)
func main() {
for i := 0; i < 1000000; i++ {
go func() {
time.Sleep(5 * time.Second)
}()
}
time.Sleep(10 * time.Second)
}
系统资源:
$ grep MemTotal /proc/meminfo
MemTotal: 4017728 kB
$ getconf LONG_BIT
64
程序运行前内存情况:
top - 12:22:31 up 1:49, 2 users, load average: 0.02, 0.01, 0.00
Tasks: 117 total, 1 running, 116 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 4017728 total, 3259556 free, 140280 used, 617892 buff/cache
KiB Swap: 998396 total, 998396 free, 0 used. 3637188 avail Mem
程序运行完休眠时内存情况:
top - 12:23:25 up 1:50, 2 users, load average: 0.09, 0.03, 0.01
Tasks: 119 total, 1 running, 118 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 4017728 total, 631380 free, 2767216 used, 619132 buff/cache
KiB Swap: 998396 total, 998396 free, 0 used. 1010280 avail Mem
前后free
的内存分别为 \(3259556\) 和 \(631380\),所以每个协程占用的内存为 \((3259556-631380)\div1000000=2.628176\) ,约为 2.6 kB。
三、切换开销
进程、线程和协程在进行切换时都会有一定的性能消耗,这种消耗通常被叫作切换开销。
3.1 进程切换
进程切换分为两步:
- 切换页目录以使用新的地址空间
- 切换内核栈和硬件上下文
所以在切换进程时,一定会有两个问题:
- 新的进程需要新的内存空间,将寄存器中的内容切换出来是最显著的性能消耗
- 上下文的切换会扰乱处理器的缓存机制。一旦切换上下文,处理器中所有已经缓存的内存在址在一瞬间全部作废,已缓存的页表被全部刷新,导致内存访问在一段时间内相当低效,程序运行也会变慢甚至出现卡顿。
3.2 线程切换
线程使用的是进程的内存资源,所以在切换线程时不需要切换虚拟内存空间。
但在切换上下文时,一样会耗费 CPU 时间,和进程切换的开销相差不大(几微秒)。
3.3 协程切换
协程的切换完全不同:
- 协程切换过程完全在用户空间发生。把当前协程 \(A\) 的 CPU 寄存器状态保存起来,然后将需要切换进来的协程 \(B\) 的 CPU 寄存器状态加载到 CPU 寄存器上就可以。
- 协程切换的过程要比进程和线程切换做的事更少
一次协程的上下文切换最多需要几十纳秒的时间。