一. 进程与线程的模型

并行与并发

61c3dd73-d8e5-4789-9eea-a77c8125d137

如图所示,假定一个计算机只能同时处理一个任务,有三个任务要解决。然后借此来了解并行与并发这两种方式。

1)并行:把任务同时分配给三台计算机,那同一时刻三个任务都是在执行中的。

2)并发:把任务分配给一台计算机,轮流完成一个任务的一个片段,同一时刻只有一个任务在执行中。

实际中的并行与并发往往混合在一起,比如可以把6个任务拆分为片段分配到3个计算机,如果画成类似上图的时间流程图,可以发现相对纯并发的时间流程图,该场景的任务片段在时间上会有1/2的重合部分。

另外需要注意一点,在并行与并发的概念中,往往会默认任务内部是无法知道任务被做了切片操作的,而且切片操作发生的时机也是无法预知的。所以在大多数情况下,并行与并发应该被理解为“并行与并发都是指任务在逻辑上是同时执行的”,而不去区分任务具体是并行还是并发,一般会统一称其为并发。并行并发是下文中线程的核心逻辑,但并行并发是一种更上层的高级概念,其可被用于分布式系统等更一般化的流程设计。

 

虚拟化技术

操作系统实现多程序同时运行的核心思想是“让执行中的程序以为自己独占CPU与内存”,程序无法感知到其他程序的存在,也无法对其他程序造成影响。操作系统通过某种方法为执行中的程序提供相对独立的虚拟化CPU与内存等硬件,并且对于不同性质与权限的程序(比如驱动、系统服务、游戏等),这种虚拟化的“程度”也是不同的。虚拟化技术的优势在于“偷换”程序执行时的底层环境,无需改变程序本身,保证了程序间的独立性与安全性。劣势在于为多程序间切换虚拟化环境会带来比较大的性能耗损。下面看操作系统实现进程与线程模块的虚拟化方式。

在接下来对进程与线程的讨论中,由于我目前没有操作系统相关的知识储备,所以学习操作系统编程只能通过“自顶向下”方法先掌握Python中线程与进程的编程接口,再去一步步的学习操作系统。但我仍觉得需要先从操作系统中抽出一些核心概念引导自己,从而实现整体上正确,把握核心但忽略细节。我把这些概念称为模型。

 

进程模型

这里讨论的进程不是具体操作系统进程的编程接口或实现细节,而是常用的多用户分时操作系统中进程的共性与核心逻辑,所以就不管其与操作系统可执行文件(PE/ELF)的关系,也不管其与具体操作系统实体(WIN/Linux)的关系。这里的进程指代一般操作系统的实现程序并行并发执行的一个抽象功能模块,也指代一个运行中的程序单位。

进程CPU虚拟化与调度的核心思想是“把CPU按时间分片,进程轮流使用时间片,当前进程耗尽时间片后储存上下文,然后系统给其他进程分配时间片,等该进程再次被分配时间片后,恢复其上下文并继续执行”。单CPU计算机多进程只能并发,多CPU计算机多进程可以混合并发并行。进程资源分配的核心思想是“为不同进程映射不同物理内存区域作为进程内存地址空间,并且程序以为这是整个内存地址空间”。实际的操作系统进程常常可以自己放弃时间片。并且在抢占式调度中,如有紧急进程需要运行,当前进程会放弃时间片,把CPU让给紧急进程。

操作系统中的进程往往是其他进程创建的,虽然进程间在逻辑上是平行关系,但操作系统会在内部记录这种父子进程关系,例如Linux系统中除了启动后的第一个进程,所有进程都是被其他进程通过fork函数创建。在进程中调用fork可得到该进程调用fork前状态和数据的完整拷贝,然后父子进程在fork调用处根据返回值“分叉”,因为父进程的fork函数返回0,子进程的fork函数返回该子进程的PID。另外子进程能够通过getppid系统调用得到父进程PID。Python内置库os提供对当前系统中系统调用函数的“复刻”,所以os在Windows下没有fork函数。

Python中内置multiprocessing进程库。相对于通过os库的系统调用编写多进程代码,multiprocessing对Python所支持的操作系统进行了封装,屏蔽不同操作系统的结构与系统调用等底层差异,使得开发者只需关注进程模型本身。如下面的代码所示,代码运行于父进程中,其用函数作为入口与子进程的代码。需要注意的是在Process中对全局变量的引用,会在执行到Process的start方法时变成对全局变量的拷贝。这其实非常容易理解,因为全局变量所在的主进程与start方法创建的子进程资源在资源上逻辑隔离,Python不会自作主张构造某种进程间共享资源的方法来兼容全局变量。另外根据fork函数功能也可知道让进程分叉的分叉数据发生拷贝,本身就是操作系统中的标准做法。

 

线程模型(包含线程的进程模型)

考虑到进程调度的性能开销较大,所以后来设计的操作系统为进程模块加入了线程模块,新的进程模块除了有独立资源以外,还有互相独立的许多线程,线程可以共享进程的独立资源,每个进程至少包含一个线程。系统可以把进程作为资源分配最小单位,把线程作为CPU调度最小单位,实现更细粒度的调度与资源分配。当调度发生在同进程的不同线程间,只需储存恢复相对少量的上下文,当调度发生在不同进程间的线程,就等于上述进程模型的调度。另外需强调在现代操作系统中,同进程的不同线程也是可并行执行的,但是其具体实现原理复杂在此暂不讨论。

操作系统根据CPU时间片的占用,定义了线程的运行状态,以展开其他的概念与设计。线程三态模型如下。

1)运行:指该线程当前正在使用CPU时间片,运行状态的线程要小于等于CPU核心数。

2)就绪:指该线程没有使用CPU时间片,但当前可被分配CPU时间片而进入运行状态。

3)阻塞:指该线程没有使用CPU时间片,但当前不可被分配CPU时间片进入运行状态,只有满足某些条件后进程才能从阻塞转为就绪,然后分配时间片运行。阻塞是相对复杂的状态,从表面上看来,线程阻塞的原因很多。比如线程正在获取锁、线程发起网络请求但是暂未得到响应、线程调用了编程语言的sleep函数等,都会被操作系统阻塞。

进程至少包含一个线程,进程创建后产生的第一个线程被称为主线程,所以进程是在主线程中创建了其他线程,通常把多个只有主线程的进程场景称为多进程。进程中的线程有唯一TID。在Linux系统中主要通过pthread的系统调用来提供线程创建,但Python并未给出线程创建的系统调用,并在整体上屏蔽了很多线程的细节,threading是Python线程标准库,其用法与进程标准库multiprocessing大致相同。如下代码所示,代码运行于主线程,线程可直接引用全局变量。

Python提供在线程与进程中发起阻塞的接口,比如sleep与join这样的函数。其中join的功能是在线程或线程的join调用处阻塞,直到join所在的线程或进程对象执行完毕。另外也可利用Python线程间同步的接口来实现阻塞。

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部