2 - 线程 - Windows 10 - CPython 解释器 - 多线程并行(实际并发)

发布时间 2023-03-24 16:51:44作者: Loki_Severus

@


测试环境:

操作系统: Window 10
工具:Pycharm
Python: 3.7

一、线程和进程介绍

进程基本概念

进程Process),是计算机中已运行程序的动态实体,曾经是分时系统的基本运作单位
面向进程设计的系统(如早期的Unix、Linux2.4及更早的版本)中,进程是程序的基本执行实体;
面向线程设计的系统(如当代多数操作系统、Linux2.6及更新的版本)中,进程本身不是基本运行单位,现在是线程的容器。程序只是指令、数据及其组织形式的描述,进程才是程序(指令和数据)的真正运行动态实例。

面向线程设计的系统内部解析 - 用户态/内核态

现代的操作系统一般都是面向线程的操作系统,比如说当你打开了Windows电脑的任务管理器,在进程菜单栏,你会看到一大堆程序在运行着,单击其中一个程序可以看到,这些程序下运行着的进程,这些运行着的程序被计算机分配了资源供给它们运行进程和线程。

如果我们使用线程模块去创建线程时,这里的创建的线程常指用户线程,因为这里的线程是运行在用户态的,而内核线程是运行在内核态的,也就是说用户线程处于用户态执行时,该线程所属的进程所能访问的内存空间和对象受到限制,其所处于占有的处理器是可被抢占的处于内核态执行时,则能访问所有的内存空间和对象,且所占有的处理器是不允许被抢占的

内核态与用户态的区别
内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态。因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;
当程序运行在0级特权级上时,就可以称之为运行在内核态
运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态(比如操作硬件)。

参考链接:
普通线程和内核线程
用户态和内核态的区别是什么

线程基本概念

线程Thread是操作系统能够进行运算调度的最小单位它被包含在进程之中,是进程中的实际运作单位一个线程指的是进程中一个单一顺序的控制流,一个进程可以并发多个线程,每个线程并行执行不同的任务。线程在Unix System V及SunOS中也被称为轻量进程,但“轻量进程”更多指内核线程,而用户线程则被称为“线程”。

这里说的有点怪怪的,其实当你在使用代码创建线程时,便是一个并发创建多个线程的过程,而当你在启动多线程时,便是在并行执行多个线程,这分为两个阶段 —— 创建、运行。(这里的线程并行指的是在多核CPU的情况下才有的)

正常情况下是这样的,但是下面讲的 Python 的多线程 伪多线程,为什么这么说呢?原因在于 Python 的解释器 CPython(当前最多人使用的主流Python解释器)内部的一个机制 GIL (Global Interpreter Lock,全局解释器锁)

下面先解释清楚进程线程的并发并行的具体过程,再来详解 Cpython 解释器

二、对进程线程并发并行的实际运行过程的理解:

在考虑并发与并行时,需要考虑到计算机CPU是否为单核和多核的这两种情况。

  1. 在计算机是单核CPU的情况下,所有的进程与线程都是一个并发的过程,归根究底,一个核心,在同一时刻只能处理一个线程,而多个时刻组成了一个时间段,在这个时间段内处理一个一个的线程,就是一个并发的过程,而且要知道就现在而言,进程只是线程容器只是作为一个调度单位存在而已,而不是一个运行的基本单位,过去是这样,不过到了现代已经都是以线程为程序运行的基本单位了,说到这里,就要知道一个进程不可能永远执行下去,直到进程结束,这时操作系统会使用一个调度器分配时间片给进程,在这个时间片(时间段),进程会获取到相应的资源(计算机内的硬件资源)从而有能力去运行它所属的程序,那么这时就需要用到一个一个的线程来作为执行者一个进程至少有一个线程,那就是主线程,而且在同一时刻下,单核的CPU只能处理一个线程,也就是主线程会被单核的CPU运行起来,直到进程分配到的时间片到了,才会停止运行进程所属的主线程,当进程分配到的时间片够它的主线程运行,且还有多余的时间片供给该进程所属的线程使用,看到这里,应该可以知道这个自然也就是一个线程并发的过程,那么,当该进程的子线程时间片(时间段)(使用时间)到了,就需要退回它拥有的资源(计算机的硬件资源),给其他的进程获取资源,以便后面其他进程的线程运行使用,这里就像是去网吧上网,时间到了,就会锁屏或关机,供给他人使用,当然也可以续费,不过对于计算机而言,这个续费,是需要靠自己争抢的 ——进程重新获取时间片(时间段),调度器分配时间片给进程,进程获取资源,线程通过该进程获取的资源运行,然后如果该进程被调度器分配到的时间片是不够用的,那就没办法了,只能将运行中线程的状态及数据等等都存入到内存里,等到后面轮到这个进程时,调度器分配了时间片给这个进程(获取资源的基本单位),再将该进程所属的线程的状态和数据等等从内存中拿出来,这里的线程有可能是主线程,也有可能是子线程,然后计算机单核的CPU就会从线程被暂停运行的位置开始,继续运行该进程所属的被暂停的线程,所以这就是内存存在的实际意义,后面的发展,大致就是如此循环运行,一个进程被分配资源,然后进程的主线程/子线程被分配时间片运行,无法按时完成的线程将被存入内存,等后面有时间了,再来运行该线程(主线程/子线程),进程内的线程一个接着一个的分配时间片运行,时间片完了,就再来一个进程这样分配资源,再执行该进程内的线程们,这样单核CPU并发的执行进程,并且并发的执行进程的线程们。
    就算是这样,看起来要排队的这种执行方法,会让你以为会很慢,不过那只是你的一厢情愿罢了,对于计算机而言,它的慢是在微观上的,宏观上对于我们来说,没区别,进程线程的并发只会让你看起来计算机就像是在并行一样,当然微观上的慢速度,会在一定数量的时候,在宏观层面体现出来,不过这不会那么容易体现出来,得是某个很大的数量,还有内存这也是应该考虑到的因素,内存不够,会导致进程执行,也会跟着慢的,平常计算机运行慢是因为内存不足,进程没法存储线程的执行停止数据信息,也就没法运行线程了。

  2. 计算机CPU多核的情况,前面提到的单核其实只是多核芯片的组合部分之一,每一个多核内的单个核心,对于所有的进程线程而言,其实运行方式的都是进程并发分配资源,线程并发执行,还有我们通常提到的并行其实是由CPU的多个核心来实现的,一个进程如果有多个线程(计算机程序运行的基本单位),那么其中一个线程就会在安排在同一时刻内或下一个时刻内等等对于我们来说宏观上近似于同一时刻,被分发到另外的CPU的核心,与原芯片CPU的核心同时或在下一个时刻等等对于我们来说宏观上近似于同一时刻,多个线程(如两个)共同完成并行执行的操作。
    一般而言,我们的计算机的CPU通常都是多核的CPU,该多核一般为4个核心,4个核心,可以在同一时刻内对4个线程进行处理,当然现在有一些超线程技术,可以让核心在同一时刻内多运行一个线程,也就是说4核可以在同一时刻内对8个线程进行处理,那么我们就可以在同一个时刻内给4核的CPU分配8个线程,当然这里的并不是说一个单核的核心真的可以在同一时刻内完成两个线程的处理,在我看来这应该也是对于CPU单核而言,是近似于同一时刻,前面是宏观上的同一时刻,而这里的是微观上的同一时刻。

如果这个CPU是单核的话,那么在进程中的不同线程为了使用CPU核心,则会进行线程切换,但是由于共享了程序执行环境,这个线程切换比进程切换开销少了很多。在这里依然是并发,唯一核心同时刻只能执行一个线程。
如果这个CPU是多核的话,那么进程中的不同线程可以使用不同核心,真正的并行出现了。

参考链接:
Python--多线程与并行
python 多线程并行_Python进阶:深入GIL(上篇)
进程的并发与并行,三种状态

好了,看到这里,也就对CPU的单核和多核、还有进程和线程的之间的关系有了一个大致上的认识,那么言归正传,前面大致解释了单核与多核、进程与线程的复杂关系,我真正想要分析的是 Cpython解释器,这个解释器内部存在的GIL(Global Interpreter Lock,全局解释器锁) 运行机制问题,

需要注意的是,GIL只存在于通过C语言实现的Python解释器上,即CPython上,后人为了绕过GIL的问题利用Java开发了Jpython或使用Python自己开发了自己的解释器PyPy,这些上都不存在GIL全局解释器锁的问题,但CPython才是当前最多人使用的主流Python解释器。

参考链接:
如何判断python解释器是哪一个

CPython 多线程争抢GIL —— 多线程实际是并发的而不是并行

某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。

前面提到过了,CPU是单核的情况下,是并发的过程,而这里如果是单核的情况下,显而易见,是一个进程一个进程的执行(分配资源),进程创建的多个线程也是一个一个的并发执行(运行程序),那么如果当python的解释器是Cpython解释器,其内部存在一个GIL(Global Interpreter Lock,全局解释器锁)运行机制,即在多核CPU的情况下,一个进程只能有一个GIL,所以即使进程想要分发线程到其他的芯片核心去运行子线程,实现线程并行,是不可能的,因为多个线程想要进入CPU运行,但是只有一个GIL,所以多核CPU一次只能进去一个线程,所以Cpython解释器下的线程并行是不可能的,是个伪线程并行

一个住户(一个进程)只有一个 通行证(一把钥匙),却要分给多个人(多个线程)使用,一个一个的传递(争抢)通信证(并发)。
看到这里就感觉很奇妙了,是吧,明明说是多线程,却是个假的多线程,实际是按照并发的方式去执行多个线程,每一个线程获取 GIL ,是需要通过争抢(并发)的,还有就算抢到了GIL,一个线程不可能会永远占用 GIL 的,它会在系统分配的时间片内释放 GIL ,还有如果线程在这个时间内还没执行完毕,释放完GIL,依旧会继续和其他线程抢 GIL

三、Cpython 解释器 GIL - 多线程总结:

Cpython 解释器不可能实现多线程并行,原因在于 GIL (Global Interpreter Lock,全局解释器锁)内部运行机制,使用了该机制,完全无法实现多线程的并行,但是我们可以绕过去,这一点可以用多进程并行来实现,因为对于Cpython解释器而言,一个进程只能有一个 GIL,那么就可以用多个进程来规避GIL,以此实现Cpython解释器下的多线程并行的相似功能。
一个进程至少有一个线程 —— 主线程,那么多个进程也就代表了多个线程(主线程),从而能实现多线程并行的目的。具体的,后面会再出一个Cpython解释器下的多进程并行的技术实现详解。
总之,Cpython解释器下的多线程是个美丽的误会,虽然很伤脑,但也是有办法解决的 —— 多进程并行