Python高并发与高性能编程:原理与实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.1 线程实现方式的改进

对于线程的实现方式,暂且抛开Python 2.X和Python 3.X版本不谈,纵观主流的Python实现方式以及CPython虚拟机或Python解释器的实现过程,可以得出,Python对于线程的实现似乎并不是作为一种主要的工作去对待,而是将其作为主分支工作之外的附加分支去处理。笔者从Python官方社区和主流使用Python的大厂中了解到,Python官方并不想让使用Python的用户因为线程的使用而感到困惑,但是线程这一概念又不得不在Python中实现。

Python语言全局解释锁的加持,使得线程实现并不会像Java语言那样复杂,也并不会像Java语言那样对外提供很多操作线程或多线程的API规范,而是提供一些基础的操作线程的API规范。Python这种设计使得开发者在开发或实现多线程或高并发的服务接口时,并不像想象中的那么顺利。但是笔者认为,Python官方对线程的这种设计方式有自己的道理,最直观的感受是从事Python开发工作的人员在初步学习Python线程开发时,容易入手,只需要1行代码就能创建出一个新线程。

通过对主流Python解释器或CPython虚拟机的观察得知,Python对外发布的第一个版本中就已经存在针对线程实现的设计,即thread.c。这是一个用C语言实现的线程文件。但是当时,使用线程的企业少之又少,而且我国刚刚引入计算机,对于Python线程模块的使用可以说是一个“黑暗时代”。在Python中线程模块真正被人们广为使用,是从Python 2.X版本开始的。在Python 2.X版本中,Python官方对早期的thread.c文件做了大量改动和更新,并且在CPython官方社区中,关于Python线程模块的issue(问题)和pull request(拉取请求)逐渐增多,CPython官方社区对这些issue(问题)和pull request(拉取请求)的解决也变得更为关注。经过对Python线程模块的长期使用和维护,以及Python官方对这些issue(问题)和pull request(拉取请求)的关注和解决,Python 3.X版本中的线程模块得到了重大改善。

由于早期的Python版本太过久远,很多有关线程的实现和对外提供的线程API早已过时,所以本章会基于比较新的Python 2.X版本对比介绍Python 3.X版本的线程实现方式。本章选择最新的Python 2.7.18以及Python 3.9.13版本进行分析。

Python 2.7.18版本中的线程模块组成如图2-1所示。

图2-1 Python 2.7.18版本中的线程模块组成

初看Python 2.7.18版本中的线程模块,似乎并没有严格的逻辑关系,这里我们对图2-1根据CPython虚拟机的实现来做进一步梳理。梳理过后的Python 2.7.18版本中的线程模块组成如图2-2所示。

对Python 2.7.18版本中的线程组成模块梳理完毕后,接着我们来看Python 3.9.13版本中的线程模块组成,如图2-3所示。

图2-2 梳理后的Python 2.7.18版本中的线程模块组成

图2-3 Python 3.9.13版本中的线程模块组成

和梳理Python 2.7.18版本中的线程模块一样,我们对Python 3.9.13版本中的线程模块做进一步梳理,会得到图2-4所示的结构。

图2-4 梳理后的Python 3.9.13版本中的线程模块组成

从图2-2和图2-4中可以得出,无论什么版本的Python线程模块,都离不开线程实现的主文件——thread.c,该文件是通过C语言实现的,并且其中的定义几乎囊括了Python语言中与线程相关的所有行为,包括线程的组成、线程的创建、线程的生命周期等,而位于thread.c文件下级的所有以.h结尾的文件,都是为了辅助thread.c文件而从Python线程实现中抽离出来的,作为Python线程的辅助实现文件。通过拓展名可以推断出,这些文件都以头文件的形式存在,且都已经被引入线程实现的主文件thread.c中。我们先来看Python 2.7.18版本中这些文件是如何被引入的,引入部分的伪代码如下所示。

在线程主文件thread.c中,对与线程相关的从.h结尾的头文件的引入是没有严格顺序的。这里对这些以.h结尾的头文件根据线程主文件thread.c的实现思路和语义做了顺序调整,大体的使用顺序见上述的代码片段。上述代码片段以空格为分隔符,对不同的头文件进行分割,目的是方便读者更好地阅读代码。下面我们进行具体分析。

首先来看引入头文件的第一部分。

这里的Python.h头文件是CPython官方为Python语言定义的基础数据支撑和基础规范支撑头文件。该文件中包含对Python对象的定义、Python内存管理的定义、Python各数据类型的定义,以及Python中各种异常或错误的定义等。由于Python.h头文件并不是我们介绍的重点,这里仅做简短介绍。对于本书来说,大家只需要了解Python.h头文件中包含的内容即可,感兴趣的读者可以自行对Python.h头文件做深入了解。

Python.h头文件中的定义几乎作用于每一个实现Python语言的特性文件,并在这些特性实现的最开始的位置进行引入,以此来告诫每一位学习CPython源码的朋友,这些文件中的内容都会受到Python.h头文件定义的约束,并且实现Python语言的特性文件也都会引用到Python.h头文件中的内容。对于线程主文件thread.c来说,它会用到Python.h头文件中定义的Python对象,并为Python对象添加线程标记和分配相应的线程空间,从而将其加入计算机操作系统的线程队列,并交由计算机操作系统进行管理(包括对Python对象线程上下文的定义、线程上下文切换的管理,以及对Python对象线程生命周期的管理。

接下来引入pthread.h和pythread.h头文件,pthread.h并不是实现Python语言特有的头文件,而是一种线程实现标准的头文件。CPython在实现Python语言时使用了这种线程实现的标准,并基于该标准实现了只属于Python语言的Python线程。这种线程标准被称为POSIX Threads标准,一般被称为POSIX线程标准。该标准最早应用于Linux系统,即一些基于Linux内核的系统,现在已可以应用于Windows 32位系统。CPython使用POSIX线程标准来构建Python线程,包括Python线程的创建、Python线程的使用、Python线程的销毁、Python线程间通信等。CPython将整个pthread.h头文件直接引入线程的实现过程。在Python线程实现时,CPython会调用pthread.h头文件中为不同操作系统提供的线程操作接口,从而实现对应操作系统下的Python线程。对于pythread.h头文件的引用,CPython根据pthread.h头文件实现,并通过基于C语言拓展的方式实现真实的Python线程头文件。pythread.h头文件中规定了Python所支持的全部线程操作,包括初始化Python线程、创建新的Python线程、退出Python线程、获取当前正在执行的Python线程、分配Python线程锁空间、获取Python线程锁操作、释放Python线程锁操作、获取Python线程栈深度、设置Python线程栈深度、设置Python线程阻塞和非阻塞标记、设置TLS(线程本地存储)API。Python线程主文件thread.c中的大多数实现都是基于pythread.h头文件完成的。

最后我们来看其他引入模块——thread_sgi.h、thread_solaris.h、thread_lwp.h、thread_pth.h、thread_pthread.h、thread_cthread.h、thread_nt.h、thread_os2.h、thread_beos.h、thread_plan9.h、thread_atheos.h,这些头文件均是辅助thread.c文件实现Python线程的头文件。

● thread_solaris.h、thread_os2.h、thread_beos.h、thread_atheos.h头文件分别针对Solaris系统、OS2系统、BeOS系统、AtheOS系统的Python线程实现。

● thread_pthread.h头文件是Python语言对POSIX线程标准的实现,包括实现对Python线程栈内存空间的分配、Python线程锁标记位的设置、Python线程的创建等。

● thread_cthread.h头文件是使用C语言实现的Python线程基础API支持库,定义了与Python线程初始化、Python线程对锁的支持等相关的基础线程接口规范。

● 除了上述头文件,剩下的头文件都是CPython针对不同的业务环境对Python线程的不同实现。

至此,Python 2.7.18版本中线程实现核心文件thread.c中的头文件就介绍完了。下面我们来看在Python 3.9.13版本中线程实现核心文件thread.c中的头文件,代码如下所示。

从上述代码清单可以清晰地看到,Python 3.9.13版本中的线程主文件thread.c并不像Python 2.7.18那样引入很多以.h结尾的头文件。对于线程主文件thread.c来说,Python 2.7.18版本与Python 3.9.13版本的最大区别是:前者对功能模块部分进行了抽离,同时针对不同的操作系统做了不同的实现,最后通过整合的方式实现Python线程;后者将旧版本中实现Python线程过程所涉基础变量和基础实现思路进行统一整合,且不会专门针对某个系统构建不同的Python线程实现。

可以看到,无论Python 2.7.18版本还是Python 3.9.13版本,都会引入Python.h头文件。关于该头文件的作用,已经在前文进行了介绍,这里不再赘述。新引入的pycore_pystate.h头文件是专门针对包含Python线程状态及线程状态管理在内的Python对象状态统一管理实现文件。该头文件不仅规定了Python线程状态的实现和管理方法,还规定了一些有关Python对象的状态管理和实现方法。Python 3.9.13与Python 2.7.18中线程的状态管理和实现方法不同,对于Python线程的状态管理和实现不会拆分成若干个头文件,而是封装到统一的pycore_pystate.h头文件中。这一手段体现了CPython官方未来在Python实现过程中对模块化概念的实践和落地,也说明了未来Python语言升级和迭代方式。

pycore_structseq.h头文件并不是为了实现Python线程而定义的。在官方发布中,该头文件虽然被引入线程主文件thread.c,但是官方给定的定位为other API,并不是Python线程实现中的任何API。关于这个头文件,大家只需要简单了解。

可以看出,关于POSIX线程标准的实现,在Python 3.9.13版本中并没有太大的差异和改动,还是通过语言定义符来将pthread.h文件引入。

但是,Python 3.9.13对于thread_pthread.h和thread_nt.h头文件的引入则是换了一种方式:通过固定的条件灵活地判断需要引入thread_pthread.h还是thread_nt.h,而不是像在Python 2.7.18中那样将这两个头文件都直接引入,再根据上述引入条件进行判断。如果_POSIX_THREADS和NT_THREADS都没有被定义,Python会抛出错误,表明没有本地线程可用。

Python 3.9.13在Python 2.7.18版本的基础上对thread_nt.h头文件进行了完善和修正,将Python 2.7.18版本中的一些内容通过升级的方式完善到了该头文件中,代码量从之前的300多行升级到了目前的600多行。这也是在Python 3.9.13版本中Python线程实现变得更加模块化,实现过程变得更加清晰的原因。

Python 3.9.13在CPython旧版本基础之上对thread_pthread.h头文件进行了完善和修正。CPython对POSIX线程标准的实现进行了扩充,引入操作系统线程实现判断模块。这一模块在Python 2.7.18中对不同操作系统线程实现的头文件进行整合,通过统一的对不同操作系统线程实现的判断,加快Python线程的创建速度。

在介绍完Python 2.7.18和Python 3.9.13中线程实现基础之后,我们以如何对Python线程初始化为例来介绍对Python线程实现方式的改进。

在Python2.7.18中,Python线程初始化代码如下。

可以看出,在Python 2.7.18中,初始化线程的函数名被声明为PyThread_init_thread,这表明该函数是对Python线程进行初始化的方法。PyThread_init_thread函数不接收任何变量,并且无任何返回值。

在PyThread_init_thread函数中,首先对当前的Python存活模式进行判断,如果当前Python进程的存活模式是DEBUG,则会通过Py_GETENV函数来获取Python当前上下文属性中名为PYTHONTHREADDEBUG所对应的内存地址,并用一个指向char类型的指针p来存储,接着会判断变量p自身是否为true——当前Python的上下文是否是PYTHONTHREADDEBUG环境,如果是,则再次判断指向char类型的指针p所指向的内存地址是否存在,如果存在,则调用C库中的atoi函数,将变量p的数据转为int类型并赋值给thread_debug变量,否则就直接将1赋值给thread_debug变量。接着,如果当前Python进程的存活模式不是DEBUG,则会首先判断Python线程的初始化变量initialized是否已经进行了初始化,如果已经进行了初始化,则直接返回,否则将数值1 手动赋值给initialized变量,并调用PyThread__init_thread函数对Python线程进行初始化,以此反复执行上述操作,直到完成Python线程初始化。

在Python 3.9.13中,Python线程初始化代码如下。

可以看出,在Python 3.9.13中移除了对当前Python进程存活模式是否是DEBUG的判断,直接对Python线程的初始化变量initialized进行判断,这里还保持了在Python 2.7.18中的判断逻辑,故不再赘述。移除对当前Python进程存活模式是否是DEBUG的判断的根本原因是,CPython官方觉得这个判断在对Python线程初始化时并不会起到任何作用,也就是说当前Python进程的存活模式是不是DEBUG并不会影响Python线程的初始化过程。