共计 2543 个字符,预计需要花费 7 分钟才能阅读完成。
一、并发和并行
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 寄存器上就能够。
- 协程切换的过程要比过程和线程切换做的事更少
一次协程的上下文切换最多须要几十纳秒的工夫。