进程、线程以及协程的区别

一、并发和并行

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 进程切换

进程切换分为两步:

  1. 切换页目录以使用新的地址空间
  2. 切换内核栈和硬件上下文

所以在切换进程时,一定会有两个问题:

  1. 新的进程需要新的内存空间,将寄存器中的内容切换出来是最显著的性能消耗
  2. 上下文的切换会扰乱处理器的缓存机制。一旦切换上下文,处理器中所有已经缓存的内存在址在一瞬间全部作废,已缓存的页表被全部刷新,导致内存访问在一段时间内相当低效,程序运行也会变慢甚至出现卡顿。

3.2 线程切换

线程使用的是进程的内存资源,所以在切换线程时不需要切换虚拟内存空间。

但在切换上下文时,一样会耗费 CPU 时间,和进程切换的开销相差不大(几微秒)。

3.3 协程切换

协程的切换完全不同:

  1. 协程切换过程完全在用户空间发生。把当前协程 \(A\) 的 CPU 寄存器状态保存起来,然后将需要切换进来的协程 \(B\) 的 CPU 寄存器状态加载到 CPU 寄存器上就可以。
  2. 协程切换的过程要比进程和线程切换做的事更少

一次协程的上下文切换最多需要几十纳秒的时间。

posted @ 2021-03-28 13:17  thepoy  阅读(278)  评论(0编辑  收藏  举报