操作系统中线程与进程概念解惑

这篇文章总结本来记录于两年前,是在写Python多线程时对一些概念的疑惑的解答,当初查阅了很多资料,对于操作系统层面的线程进程概念很模糊。而今,随着Python版本的更新,本身异步的新特性逐渐完善,在学习异步的时候难免与常用的多进程多线程进行比较来说明异步的优越性,把以前的文章拿出来,并且在现在自己的理解上查阅一些资料进行适当修改,达到温故而知新的效果。

看了一天的相关概念,很多涉及到操作系统与底层硬件层面,脑子有点晕,对自己所理解的东西清理一下并记录下来,有些不对的地方还请指出.

1. 线程与进程

进程

我们都知道计算机的核心是CPU,它承担了所有的计算任务;而操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;应用程序侧是具有某种功能的程序,程序是运行于操作系统之上的。 (为了缓解头脑胀痛, 斜体字大体过一遍即可)
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序、数据集合和进程控制块三部分组成。程序用于描述进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需要的数据和工作区;程序控制块(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。

线程

在早期的操作系统中并没有线程的概念,在当时进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。进程间的通信与同步完全袭来操作系统内核转发与支持。
后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。

进程与线程的区别

  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位 (这句话一定要理解);
  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
  3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
  4. 调度和切换:线程上下文切换比进程上下文切换要快得多。

线程与进程关系的示意图:
线程与进程关系

某个进程中单线程与多线程的关系:
单线程与多线程


从别处看到线程是进程的一个实体,是程序执行的最小单位(线程也被称为轻量级进程).
按照我的理解:QQ音乐正在运行,QQ音乐就是个进程,而这个进程中有多个线程在跑着(各有各的任务);我们平常编写的所有的Python程序,都是执行单任务的进程,也就是只有一个线程。
如果我们要同时执行多个任务怎么办:一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务;还有一种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务(之后详细讲)。

2. 线程与cpu核心数的关系

现在都是多核的处理器,那么在多核处理器的情况下,线程是怎样执行呢?这就需要了解内核线程。

多核(心)处理器是指在一个处理器上集成多个运算核心从而提高计算能力,也就是有多个真正并行计算的处理核心,每一个处理核心对应一个内核线程。内核线程(Kernel Thread, KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。一般一个处理核心对应一个内核线程,比如单核处理器对应一个内核线程,双核处理器对应两个内核线程,四核处理器对应四个内核线程。
现在的电脑一般是双核四线程、四核八线程,是采用超线程技术将一个物理处理核心模拟成两个逻辑处理核心,对应两个内核线程,所以在操作系统中看到的CPU数量是实际物理CPU数量的两倍,如你的电脑是双核四线程,打开“任务管理器\性能”可以看到4个CPU的监视器,四核八线程可以看到8个CPU的监视器。

上面的应该可以理解,但是有个注意点:程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程. 那为什么要说出上面那段文字呢? 我心中的困惑是:一般我们买的CPU包装盒上写的 四核八线程的”线程” 到底是什么线程?跟系统的线程是一个东东么?看这里

由于每个轻量级进程都需要一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。
下图中:K代表内核线程,LWP代表轻量级线程,U代表 用户线程

内核线程用户线程

问题一:什么是用户线程?

用户线程是完全建立在用户空间的线程库(比如python的threading库),用户线程的创建、调度、同步和销毁全又库函数在用户空间完成,不需要内核的帮助。因此这种线程是极其低消耗和高效的。

问题二:请简单解释下图中流程?

还是上面的例子,当你运行一个python脚本时,等于创建可一个进程,光有进程没卵用,必须有程序执行的最小单位嘛,所以同时系统会创建一个LWP,程序就靠它运行,因为是第一个线程,也称它为主线程。然后,脚本跑着跑着,需要创建新的线程运行其他任务时,主线程程序安排一个”创建线程“的口号(比如调用一个线程库)来调用产生U。
可以看这里的回答,进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。

问题二:系统中有时会有那CPU核心数的数量影响着什么嘛?

(不是很确定),多核就是系统同时可以运行多个线程,比如双核可以同时执行两个线程。单核只能一次执行一个线程。
如果答案真如上面那样:那为什么能有百来个多线程同时存在?那么就可以引申到下面的话题了。

3. 并发和并行

并行(parallelism)

这个概念很好理解。所谓并行,就是同时执行的意思,无需过度解读(在日本跟台湾,翻译成平行,这就更好理解了)。判断程序是否处于并行的状态,就看同一时刻是否有超过一个“工作单位”在运行就好了。所以,单线程永远无法达到并行状态。
要达到并行状态,最简单的就是利用多线程和多进程。但是 Python 的多线程由于存在著名的 GIL,无法让两个线程真正“同时运行”,所以实际上是无法到达并行状态的。

那么对于上面的问题三,我们是否可以认为,一个四核八线程的CPU,最多允许八个线程同时执行?某个时间点就只有八个,然后下一时间,其他八个再执行=.=(不展开。有点晕)

在并行性开发时,不但可以考虑多进程并行执行,而且还可以开发出进程内多线程并行的程序,如下图。
并行执行实现方法





并发(concurrency)

要理解“并发”这个概念,必须得清楚,并发指的是程序的“结构”。当我们说这个程序是并发的,实际上,这句话应当表述成“这个程序采用了支持并发的设计”。好,既然并发指的是人为设计的结构,那么怎样的程序结构才叫做支持并发的设计
正确的并发设计的标准是:使多个操作可以在重叠的时间段内进行(two tasks can start, run, and complete in overlapping time periods)。

这句话的重点有两个。我们先看“(操作)在重叠的时间段内进行”这个概念。它是否就是我们前面说到的并行呢?是,也不是。并行,当然是在重叠的时间段内执行,但是另外一种执行模式,也属于在重叠时间段内进行。这就是协程(或者简单点多线程也可以,然后把task1,task2改成线程1,线程2)。
使用协程时,程序的执行看起来往往是这个样子:

协程

task1, task2 是两段不同的代码,比如两个函数,其中黑色块代表某段代码正在执行。注意,这里从始至终,在任何一个时间点上都只有一段代码在执行,但是,由于 task1 和 task2 在重叠的时间段内执行,所以这是一个支持并发的设计。与并行不同,单核单线程能支持并发(特指协程)。

那么,如何实现支持并发的设计?两个字:拆分。
之所以并发设计往往需要把流程拆开,是因为如果不拆分也就不可能在同一时间段进行多个任务了。这种拆分可以是平行的拆分,比如抽象成同类的任务,也可以是不平行的,比如分为多个步骤。

并发和并行的区别
并行指物理上同时执行,并发指能够让多个任务在逻辑上交织执行的程序设计

3. 同步和异步理解

这里我就简单举个例子就好了:web网页,点一下发送邮件按钮,然后发送(发送过程5秒),发送成功之网页显示跳转成功。
同步:点了发送之后,慢慢等五秒,然后显示成功
异步:点了发送之后,直接显示成功,后台默默的还在发送,知道发送完毕。
关于阻塞啥的我这里就不提了,已经有些晕了。毕竟在python中有些集成的库已经将这些问题考虑进去了,不需要重复造轮子,具体可以看看这篇文章前半部分

5. python的相关模块

  • threading 多线程库
  • concurrent.futures中的ThreadpoolExecutor 线程池
  • concurrent.futures中的ProcesspoolExecutor 进程池
  • subprocess 子进程
  • yield关键字 协程
  • asyncawait (python3.5新加入) 协程库

6. 其他我想要说的几个点

  1. 在多线程这方面我代码中用的比较少.有时偶尔用到,我会直接选用ThreadpoolExecutor类。这个接口抽象层级很高,多线程直接使用,无需关心任何实现细节,啥线程死锁等问题都不需要担心。

  2. 多线程在单cpu中其实也是顺序执行的,不过系统可以帮你切换那个执行而已,其实并没有快(反而慢);多个cpu的话就可以在两个cpu中同时执行了。

  3. 接上面那一条,虽然python有GIL锁,启用多线程的时候只能用一个cpu核心,但python线程仍然适合I/O密集型应用:标准库中每个使用 C 语言编写的 I/O 函数都会释放 GIL,因此,当某个线程在等待 I/O 时, Python 调度程序会切换到另一个线程。 (意思就是说I/O操作的延迟大于过程中程序运行时间,我完全可以在延迟的时候,切换到其他线程,做其他操作,如下图情况C)
    单线程多线程

  4. 多线程与协程的区别:协程执行效率极高,因为子程序切换(函数)不是线程切换,由程序自身控制,不像多线程那样要切换线程的开销。所以与多线程相比,线程的数量越多,协程性能的优势越明显。

相关链接:
https://laike9m.com/blog/huan-zai-yi-huo-bing-fa-he-bing-xing,61/
https://lz5z.com/Python%E5%8D%8F%E7%A8%8B/
http://blog.csdn.net/luoweifu/article/details/46595285
http://yangcongchufang.com/%E9%AB%98%E7%BA%A7python%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80/python-process-thread.html
http://blog.csdn.net/gatieme/article/details/51481863
http://www.cnblogs.com/caihuafeng/p/5438753.html