多进程并发为什么没有达到预期的性能

1. 引言

前面的文章中,我们介绍了 python 中的进程与线程模型。
我们看到,由于 GIL 锁的存在,python 中的线程效率并不高,也不能利用多核 CPU 的特性,与多线程并发相比,多进程并发显得更有优势。
可是经过我们的测试,多进程并发的执行效率也没有我们想象中的那么高,那么,究竟是什么原因造成了多进程并发性能的下降呢?

2. 进程与线程的区别

进程是一个程序的一次执行,而线程则是 CPU 的最小调度单位。
每个进程中可以包含一个或多个线程,多个线程共享进程地址空间中的全部资源,这也就是为什么线程也被称作“轻量级进程”,因为下面这些信息都保存在进程地址空间中,所有线程共享:

  1. 全局变量

  2. 打开的文件

  3. 子进程地址空间

  4. 信号与响应函数

  5. 用户信息

线程内只保存自己的堆栈、寄存器、程序计数器以及线程自身的状态信息等信息。
所以线程本身相较于进程而言是非常轻量的。

3. 上下文切换

CPU 的每个核心在同一时间只能执行一条指令,多进程的并发执行依赖于 CPU 对任务的反复切换,任务的执行单位是 CPU 的“时间片”,在两个时间片之间,CPU 就必须进行上下文切换,来加载进程运行所必须的数据。
进程运行过程中所依赖的数据,包括进程地址空间、CPU寄存器、程序计数器、文件描述符等信息就是进程运行的上下文,在下面的三个事件发生时,CPU 就必须进行一次上下文切换:

  1. 中断处理

  2. 多任务切换

  3. 用户态切换

上下文切换就是我们上面所提到的多进程并发过程中性能下降的元凶,那么,究竟上下文切换做了什么呢?

4. 用户态与内核态

Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着下图中的 ring0 与 ring3。

  • 内核空间(Ring 0)具有最高权限,可以直接访问所有资源

  • 用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源

进程在用户空间运行就被称为用户态,在内核空间运行则被称为内核态,从用户态到内核态的转变依赖于操作系统的陷阱机制,本质上,陷阱机制是一种软件中断机制,让用户态进程可以中断执行,陷入内核态进行一些高优先级操作。
对于进程上下文切换来说,系统需要操作底层的寄存器、存储设备等只有内核才可以操作的资源,因此上下文切换只能发生在内核态,因此首先需要进行一次从用户态陷入内核态的模式切换,紧接着,内核需要保存所有被切换进程的执行信息,包括寄存器数据、打开的文件描述符、进程地址空间等,然后载入接下来需要执行的进程的上述信息。

5. 虚拟地址空间的切换

上一篇文章中,我们介绍了操作系统的分段与分页机制。

操作系统中,由内存管理单元 MMU 实现的页面置换算法实现了分页机制,从而让每个进程都拥有独立的进程地址空间。
每个进程都保存了一份虚拟地址与物理地址的映射关系,这个映射关系就是页表。
页表的存在,让进程中可以使用抽象的虚拟地址而不是实际的物理地址,但如果每次都查询多级页表显然是十分耗时的,因此在 CPU 中拥有一块特殊的缓存 — TLB(Translation lookaside buffer),他用来缓存一部分热点页表,让系统可以直接查询到页表所在的物理地址。
当进程上下文切换时,显然,TLB 中缓存的信息也随之失效,系统被迫到内存中查找多级页表来寻找需要使用的内存页面的物理地址,性能也就随之产生了巨大的下降。

6. 微信公众号

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤。

阅读原文

多进程并发为什么没有达到预期的性能》来自互联网,仅为收藏学习,如侵权请联系删除。本文URL:http://www.bookhoes.com/820.html