《Python源码剖析》读书笔记

Posted by xiezg247 on April 16, 2018

Python源码剖析学习笔记

一. Python源码剖析——编译Python

Python总体架构

在最高的层次上,Python的整体架构可以分为三个主要的部分,如上。

图左,是Python提供的大量的模块/库以及用户自定义的模块。比如在执行import os时,这个os是Python内建的模块,用户还可以通过自定义模块来扩展Python系统。

图右,是Python的运行时环境,包括对象/类型系统(Object/Type structures),内存分配器(Memory Alloator)和运行时状态信息(Current State of Python)。

  • 对象/类型系统则包含了Python中存在的各种内建对象,比如int/list/dict,以及用户自定义的各种类型和对象。

  • 内存分配器则全权负责Python中创建对象时,对内存的申请工作,实际上它就是Python运行时与C中malloc的一层接口。
  • 运行时状态维护了解释器在执行字节码时不同的状态之间的切换的动作,可视它为一个巨大而复杂的有穷状态机。

图中,是Python的核心——解释器(interpreter),或者称为虚拟机。在解释器中,箭头的方向指示了Python运行过程中的数据流方向。

  • Scanner对应词法分析,讲文件输入的Python源代码或者从命令行输入的一行行Python代码切分为一个个的token
  • Parser对应语法分析,在Scanner的分析结果上进行语法分析,建立抽象语法树(AST)
  • Compiler是根据建立的AST生成指令集合——Python字节码
  • Code Evaluator执行以上的字节码,因此,Code Evaluator又可称为虚拟机

以上,在解释器与右边的对象/类型系统,内存分配器之间的箭头表示“使用”关系;与运行时状态之间的箭头表示“修改”关系,即是Python在运行的过程中会不断修改当前解释器所处的状态,在不同状态之间切换。

Python源代码的组织

Include:该目录包含了Python提供的所有头文件,如果用户需要自己用C或C++来编写自定义模块来扩展Python,那么需要这里提供的头文件。

Lib:该目录包含了Python自带的所有标准库,Lib中的库都是用Python语言写的。

Modules:该目录包含了所有用C语言编写的模块,Modules中的模块时那些对速度要求非常严格的模块,而有一些对速度没有太严格要求的模块,则用Python语言编写。

Parser:该目录包含了Python解释器中的Scanner和Parser,即对Python源代码进行词法分析和语法分析的部分。除了这些,Parser目录下还包含了一些有用的工具,这些工具可以根据Python语言的语法自动生成Python语言的词法和语法分析器。

Objects:该目录包含了Python的所有内建对象,包含整数/list/dict等。同时,该目录还包括了Python在运行时需要的所有的内部使用对象的实现。

Python:该目录包含了Python解释器中的Compiler和执行引擎部分,是Python运行核心所在。

PCBuild:该目录包含了Visual Studio 2003使用的工程文件,研究Python源代码就从这里开始。

PCBuild8:该目录包含了Visual Studio 2005使用的工程文件。

二. Python对象初探

Python内的对象

1. 对象的概念
  • 对于人的思维来说,对象是一个形象的概念,而对于计算机,对象却是一个抽象的概念,它所知道的只是字节。
  • 通常来说,对象是数据以及基于这些数据的操作的集合。
  • 在计算机中,一个对象实际上就是一片被分配的内存空间,这些内存有可能是连续的,也有可能是离散的,在更高层次上,这些内存可以当作一个整体来考虑,这个整体就是对象。
  • 在这片内存中,存储着一系列的数据以及可以对这些数据进行修改或者读取操作的一系列代码。
2. 对象的特点
  • 在Python中,对象就是为C中的结构体在堆上申请的一块内存。
  • 在Python中,所有的内建的类型对象都是被静态初始化的。(一般来说,对象是不能被静态初始化的,并且不能在栈空间上生存。唯一的例外就是类型对象)
  • 在Python中,一个对象一旦被创建,它在内存中的大小就是不变的了。(这就意味着那些需要容纳可变长度数据的对象只能在对象内维护一个指向一块可变大小的内存区域的指针)
3. 对象的分类

Python的对象从概念上可以大致分为五类,这种分类不一定正确,不过可以提供另外一个角度看待Python中的对象。

  • Fundamental 对象:类型对象

  • Numeric 对象:数值对象

  • Sequence 对象:容纳其他对象的序列集合对象

  • Mapping 对象:类似于C++中的map的关联对象

  • Internal 对象:Python的虚拟机在运行时内部使用的对象

4. 对象机制的基石

在Python中,所有的东西都是对象,而所有的对象都拥有一些相同的内容(这句话的另外意思是,每一个Python对象除了必须有这个PyObject内容外,还占有额外的内存,放置其他内容),这些内容在PyObject中定义,出现在每一个Python对象所占有的内存的最开始的字节中,PyObject是整个Python对象机制的核心。

PyObject定义如下:

  • 在PyObject定义中,整型变量ob_refcnt与Python的内存管理机制有关,它实现了基于引用计数的垃圾回收机制。
  • ob_refcnt之外,还有一个ob_type指向结构体_typeobject的指针,这个结构体对应着Python内部的一种特殊对象,它是用来指定一个对象类型的类型对象。

在Python中,对象机制的核心非常简单,一个是引用计数,一个是类型信息。

5. 定长对象和变长对象

不包括可变长数据的对象称为定长对象(例如整数对象)。

包括可变长数据的对象称为变长对象(例如字符串对象)。

区别在于定长对象的不同对象占用的内存大小是一样的,变长对象的不同对象占用的内存大小是不一样的。

6. 可变对象和不可变对象

可变对象是一旦创建后内容还可改变,但是地址不会发生改变,即该变量指向原来的对象。(例如list,dict)

不可变对象是一旦创建后内容不可改变,如果更改,则变量会指向一个新的对象。(例如int,string,float,tuple)

类型对象

1. 对象的元信息

结构体_typeobject

_typeobject的定义中包含了很多信息,主要分为四类:

  • 类型名,tp_name
  • 创建该类型对象时分配的内存空间大小信息,tp_basicsizetp_itemsize
  • 与该类型对象相关联的操作信息,tp_print等函数指针
  • 描述该类型对象的类型信息
2. 对象的创建

一般来说,Python创建对象时会有两种方法,一种是通过Python C API来创建,一种是通过类型对象PyInt_Type

Python C API分成两类:

一类称为范型的API,或者称为AOL(Abstract Object Layer)。这类API都具有PyObject_***的形式,可应用在任何Python对象上。

另一类是与类型相关的API,或者称为COL(Concrete Object Layer)。这类API通常只能作用在某一种类型的对象上,对于每一种内建对象,Python都提供了这样的一组API。

无论是使用哪一种Python C API,Python内部都是直接分配内存的。

3. 对象的行为

在PytypeObject中定义了大量的函数指针,这些函数指针最终会指向某个函数,或者指向NULL。这些函数可以视为类型对象中所定义的操作,而这些操作直接决定着一个对象在运行时所表现出的行为。

4. 类型的类型

Python的类型对象PyTypeObject也是一个对象,是由PyType_Type创建的。

PyType_Type是Python类型机制中一个至关重要的对象,所有用户自定义class所对应的PyTypeObject对象都是由这个对象创建。

PyType_Type是所有class的class,在Python中被称为metaclass

多态性

在Python中创建对象,比如PyIntObject对象时,会分配内存,进行初始化。Python内部会用一个PyObject *变量来保存和维护这个对象,而不是PyIntObject *,其他对象也与此类似。

因此,在Python内部各个函数之间传递的是一种范型指针PyObject *,这个指针所指对象的ob_type域动态进行判断,通过这个域,Python实现了多态机制。

对象的引用计数

Python通过对一个对象的引用计数的管理来维护对象在内存中的存在与否。Python的每个对象都有ob_refcnt变量,这个变量维护着对象的引用计数,从而决定着该对象的创建与消亡。

在Python中,主要是通过Py_INCREF(op)Py_DECREF(op)两个宏来增加和减少一个对象的引用计数。当一个对象的引用计数减少到0之后,Py_DECREF将调用该对象的析构函数释放该对象所占有的内存和系统资源。

此处调用析构函数并不意味着最终会调用free函数来释放内存空间,如果这样做的话,频繁申请内存和释放内存,会导致Python的执行效率大打折扣。

一般来说,Python中大量采用了内存对象池的技术,调用析构函数时,通常是将该对象所占有的空间归还给内存池中,避免了频繁地申请和释放内存。

Tips: 在Python的各种对象中,类型对象是超越引用计数规则的,永远不会被析构,每一个对象中指向类型对象的指针不会被视为对该类型对象的引用。

三. Python中的整数对象

PyIntObject对象

1. PyIntObject对象的定义

  • Python中的整数对象PyIntObject实际上是C中原生类型long的一个简单包装。
  • Python中的对象的相关元信息实际上都是保存在对应的类型对象中的,对于PyIntObject,类型对象是PyInt_Type

PyIntObjecy对象的创建和维护

1. 对象创建的三种途径
  • 从long值生成PyIntObject对象
  • 从字符串生成PyIntObject对象
  • 从Py_UNICODE对象生成PyIntObject对象
2. 小整数对象

在Python对象中,所有的对象都是生活在堆上,如果没有特殊的机制的话,那么Python将一次又一次使用malloc在堆上申请空间和释放空间,基于此种情况,对于小整数引入了对象池技术。

在Python2.5中,将小整数集合的范围默认设定为[-5, 257],可修改NSMALLNEGINTS和NSMALLPOSINTS的值,重新编译Python,从而将这个范围向两端伸展或收缩。

3. 大整数对象

对于小整数,在小整数对象池中完全地缓存了其PyIntObject对象,而对其他整数,Python运行环境将提供一块内存空间,这些内存空间将由大整数轮流使用。

在Python中,有一个PyIntBlock结构,在这个结构的基础上,实现了一个单向列表。

4. 添加和删除

PyIntObject对象的创建通过两部完成:

  • 如果小整数对象池被激活,则尝试小整数对象池
  • 如果不能使用小整数对象池,则使用通用的整数对象池

四. Python中的字符串对象

PyStringObject对象

1. PyStringObject对象的定义

  • 对于PyStringObject,类型对象是PyString_Type
  • ob_size变量保存着对象中维护的可变长度内存的大小,ob_sval指向一段长度为ob_size+1个字节的内存。
  • ob_hash变量是缓存对象的hash值。
  • ob_sstate变量标记了该对象是否已经过intern机制的处理。

字符串对象的intern机制

PyStringObject对象的intern机制的目的是:对于被intern之后的字符串,比如说“Python”,在整个Python的运行期间,系统中只有唯一的一个与字符串“Python”对应的PyStringObject对象。

intern机制的核心是在系统中有一个(key,value)映射关系的集合,集合的名称叫做interned。在这个集合中,记录着被intern机制处理过的PyStringObject对象。

字符串缓冲池

在Python的整数对象体系中,小整数的缓冲池是在Python初始化时被创建的,而字符串对象体系中的字符串缓冲池则是以静态变量的形式存在着的。

在创建PyStringObject时,会首先检查所要创建的是否是一个字符对象,然后检查字符串缓冲池中是否已经有了这个字符的字符对象的缓冲,如果有,则直接返回这个缓冲对象即可。

PyStringObject效率相关问题

Python中一个举足轻重的问题——字符串拼接。

Python提供了“+”操作符来进行字符串拼接的功能,其效率十分低下,其根源在于Python中的PyStringObject对象是一个不可变对象,这就意味着当进行进行字符串拼接时,实际上要创建一个PyStringObject对象,如果需要连接N个PyStringObject对象,那么必须进行N-1次的内存申请搬运工作。

官方推荐的做法是通过PyStringObject对象的join操作来对存储在list或者tuple中的一组PyStringObject对象进行连接操作,这种做法只需要分配一次内存,大大提供执行效率。

执行join操作时,会先统计出list中一共有多少个PyStringObject对象,并统计这些PyStringObject对象所维护的字符串一共有多长,然后申请内存,将list中PyStringObject对象维护的字符串都拷贝到新开辟的内存空间中。

五. Python中的List对象

PyListObject对象

1. PyListObject对象的定义

  • 对于PyListObject,类型对象是PyList_Type
  • **ob_item这个指针指向元素列表所在的内存块的首地址。
  • allocated则维护了当前列表中的可容纳的元素的总数。

PyListObject对象的创建和维护

1. 创建对象

Python提供了一个PyList_New函数来创建列表,这个函数接受一个size参数,从而允许可以在创建一个PyListObject对象的同时指定该列表初始的元素个数。

2. 设置元素

假设创建一个包含6个元素的PyListObject对象,也就是通过PyList_New函数创建PyListObject对象,当完成之后,PyListObject对象的情形应该如下:

当把一个整数对象100放置到第四个位置时,Python会进行类型检查,再进行索引检查,随后将待加入的PyObject *指针放到指定位置,调整引用计数,将这个位置原来存放的对象的引用计数减1。

3. 插入元素

设置元素不会使得ob_item指向的内存发生变化,插入元素有可能使得ob_item指向的内存发生变化。

在调整PyListObject对象所维护的列表的内存时,Python分两种情况处理:

  • newsize < allocated && new size > allocated/2 简单调整ob_item
  • 其他情况,调用realloc,重新分配空间
4. 删除元素

当Python执行list.remove(3)时,PyListObject对象中的listremove操作将会被激活,对整个列表进行遍历,将待删除的元素与PyListObject对象中的每个元素一一比较,如果匹配,则删除该元素。

####PyListObject对象缓冲池

在创建一个新的list时,创建过程分为两步,首先创建PyListObject对象,然后创建PyListObject对象所维护的元素列表。

与之对应的,在销毁一个list时,先销毁PyListObject对象所维护的元素列表,然后释放PyListObject对象自身。

在删除PyListObject对象自身时,Python会检查PyListObject对象缓冲池,查看其中缓存的PyListObject的数量是否已经满了,如果没有,就将该待删除的PyListObject对象放到缓冲池中,以备后用。

在Python下一次创建新的list时,这个PyListObject对象将重新被唤醒,重新分配PyObject *元素列表占用的内存,重新分配新的对象。

六. Python中的Dict对象

关联容器

为了刻画某种对应关系,现代的编程语言通常都在语言级或者标准库中提供某种关联式的容器。关联式的容器中存储着一对对符合该容器所代表的关联规则的元素对,通常是以键值对的形式存在。

关联容器的设计总会极大的关注健的搜索效率,例如C++的STL中的map的实现是基于RB-tree(红黑树),理论上,其搜索的时间复杂度为O($\log_2$N)。

Python同样提供了关联式容器,即PyDictObject对象,采用了散列表(hash table),理论上,在最优的情况下,其搜索的时间复杂度为O(1)。

散列表概述

散列表的基本思想,是通过一定的函数将需搜索的键值映射为一个整数,将这个整数视为索引值去访问某片连续的内存区域。

用于映射的函数称为散列函数,而映射后的值称为元素的散列值。在散列表的视线中,所选择的散列函数的优劣将直接决定所实现的散列表的搜索效率高低。

散列表在使用过程中,不同的对象经过散列函数的作用,可能被映射为相同的散列值,而且随着存储数据的增加,这样的冲突就会发生的越来越频繁,散列冲突是散列表与生俱来的问题。为了解决这种问题,Python采用的是开放定址法。

PyDictObject对象

1. 关联容器的entry

我们将关联容器中的一个(健,值)元素对称为一个entry或者slot,在Python中,一个entry的定义如下:

  • PyDictObject中存放的是PyObject *,这也是Python的dict什么都能装得下的缘故,在Python中,无论什么东西都是PyObject对象。
  • PyDictEntry中,me_hash域存储的是me_key的散列值,利用一个域来记录这个散列值,可以避免每次查询的时候都要重新计算一遍散列值
  • 在一个PyDictObject对象生存变化的过程中,其中的entry会在3种状态之间切换:Unused/Active/Dummy。
    • 当一个entryme_hash域存储的是me_key都是NULL时,处于Unused态。
    • entry中存储了一个(key, value)对时,切换为Active态,me_hash域存储的是me_key都不为NULL。
    • entry中存储的(key, value)对被删除后,状态不能直接从Active态转为Unused态,此时,entry进入Dummy态,说明该entry是无效的,但其后的entry可能是有效的,是应该被搜索的。
2. PyDictObject对象的定义

在Python中,关联容器是通过PyDictObject对象来实现的,而一个PyDictObject对象实际上是一大堆entry的集合,总控这些集合的结构如下:

  • PyDictObject对象定义的最后,有一个ma_smalltablePyDictEntry数组,这个数组意味着当创建一个PyDictObject对象时,至少有PyDcit_MINSIZEentry被创建,这个PyDcit_MINSIZE值默认为8,可以改变此值来调节Python的运行效率。
  • 当一个PyDictObject对象中的entry数量小于8个,认为此对象是一个小dict,当entry数量大于8个,认为此对象是一个大dict,将会申请额外的内存空间。

PyDictObject对象的创建和维护

1. PyDictObject对象的创建

Python提供了一个PyDict_New函数来创建一个新的dict对象。

2. PyDictObject对象的元素搜索

Python为PyDictObject对象提供了两种搜索策略,lookdictlookdict_string,实际上,这两种策略使用的是相同的算法,lookdict_string只是lookdict针对PyStringObject对象的特殊形式。

PyStringObject对象作为PyDictObject对象中entry的键在Python中广泛应用,因此,lookdict_string也就成为了PyDictObject对象创建时所默认的搜索策略。

lookdict进行第一次检查时所进行的主要动作如下:

  • 根据hash值获得entry的索引,表明冲突探测链上的第一个entry的索引
  • 在两种情况下,搜索结束:
    • entry处于Unused状态,表明冲突探测链搜索完成,搜索失败
    • ep->me_key == key,表明entry的key与待搜索的key匹配,搜索成功
  • 若当前entry处于Dummy状态,设置freeslot
  • 检查Active态entry中的key与待查找的key是否“值相同”,若成立,搜索成功

如果冲突探测链上的第一个entry的key与待搜索的key不匹配,那么很自然的,lookdict会沿着探测链,依次比较探测链上的entry与待查找的key,主要动作如下:

  • 根据Python所采用的探测函数,获得探测链上的下一个带检查的entry
  • 检查到一个Unused态entry,则表示搜索失败,有两种结果:
    • 如果freeslot不为空,则返回freeslot所指向entry
    • 如果freeslot为空,则返回该Unused态entry
  • 检查entry中的key与待检查的key是否符合“引用相同”规则
  • 检查entry中的key与待检查的key是否符合“值相同”规则
  • 在遍历过程中,如果发现Dummy态entry,且freeslot未设置,则设置freeslot

因此,搜索操作在成功时:

  • 返回相应的处于Active态的entry

在搜索失败时,返回两种不同的结果:

  • 处于Unused态的entry
  • 处于Dummy态的entry
3. 插入与删除

插入操作:

  • 搜索成功,返回处于Active态的entry,直接替换me_value
  • 搜索失败,返回Unused或者Dummy态的entry,完整的设置me_value/me_hash/me_key

删除操作:

先计算hash值,然后搜索相应的entry,最后删除entry中维护的元素,并将entry从Active态变换为Dummy态,同时还将调整PyDictObject对象维护table使用情况的变量。

PyDictObject对象缓冲池

PyDictObject对象中使用的缓冲池与PyListObject对象中使用的缓冲池机制是一样的。

开始,缓冲池里面什么都没有,直到第一个PyDictObject对象被销毁时的,这个缓冲池才开始接纳被缓冲的PyDictObject对象。

PyListObject对象一样,缓冲池中只保留了PyDictObject对象。如果PyDictObject对象中ma_table维护的是从系统堆申请的内存空间,那么Python将释放这块内存空间,归还给系统;如果被销毁的PyDictObject对象中的table实际上并没有从系统申请,而是指向PyDictObject固有的ma_smalltable,那么只需要调整ma_smalltable中的对象引用计数即可。

七. Python的编译结果:code对象与pyc文件

Python程序的执行过程

Python解释器在执行任何一个Python程序文件时,首先进行的动作都是对文件中的Python源代码进行编译,编译的主要结果是产生一组Python的byte code(字节码),然后将编译的结果交给Python的虚拟机,由虚拟机按照顺序一条一条地执行字节码,从而完成对Python程序的执行动作。

Python编译器的编译结果——PyCodeObject对象

1. PyCodeObject对象与pyc文件

在程序运行期间,编译结果存在与内存的PyCodeObject对象中,而Python结束运行后,编译结果又被保存到pyc文件中。当下一次运行程序时,Python会根据pyc文件中记录的编译结果直接建立内存中的PyCodeObject对象,而不用再次对源文件进行翻译了。

对于Python编译器来说,PyCodeObject对象才是真正的编译结果,而pyc文件只是对这个文件在硬盘上的表现形式,它们实际上是Python对源文件编译的结果的两种不同存在方式。

2. Python源码中的PyCodeObject

Python编译器在对Python源代码进行编译时,对于代码中的一个Code Block会创建一个PyCodeObject对象与这段代码对应。

Python有一个简单而清晰的规则:当进入一个新的名字空间时,或者说作用域时,就算进入咯一个新的Code Block。

在Python中,类/函数/module都对应着一个独立的名字空间。

Pyc文件的生成

1. pyc文件的生成

Python在通过importmodule进行动态加载时,如果没有找到相对应的pyc文件或者dll文件,就会在py文件的基础上自动创建pyc文件。

Python的字节码

Python源代码在执行前会被编译为Python的字节码指令序列,Python虚拟机根据这些字节码来进行一系列的操作,从而完成对Python程序的执行。

在Python2.5中,一共定义了104条字节码指令。

八. Python的虚拟机框架

九. Python虚拟机中的一般表达式

十. Python虚拟机中的控制流

十一. Python虚拟机中的函数机制

十二. Python虚拟机中的类机制

十三. Python运行环境初始化

十四. Python模块的动态加载机制

十五. Python多线程机制

GIL与线程调度

Python代码的执行由Python虚拟机来控制,Python在设计之初就考虑在解释器的主循环中,同时只有一个线程在执行,即是在任意时刻,只有一个线程在解释器中运行。

对Python虚拟机的访问由全局解释器(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。

在当前2.5中,Python默认的行为是在执行了100条指令后启动线程调度机制。

在多线程环境中,Python虚拟机按以下方式执行:

  • 设置GIL
  • 切换到一个线程去运行
  • 运行:
    • 指定数量的字节码指令
    • 或者线程主动让出控制
  • 把线程设置为睡眠状态
  • 解锁GIL
  • 再次重复以上步骤

在调用外部代码的时候,GIL将会被锁定,直到这个函数结束为止(由于这期间没有Python的字节码被运行,所以不会做线程切换)。

GIL设计理念与限制

解释器被一个全局解释器锁保护着,它确保任何时候都只有一个Python线程执行。

GIL最大的问题就是Python的多线程程序并不能利用多核CPU的优势 (比如一个使用了多个线程的计算密集型程序只会在一个单CPU上面运行)。

GIL只会影响到那些严重依赖CPU的程序。

如果你的程序大部分只会设计到I/O,比如网络交互,那么使用多线程就很合适, 因为它们大部分时间都在等待。实际上,你完全可以放心的创建几千个Python线程, 现代操作系统运行这么多线程没有任何压力,没啥可担心的。

Python线程

Python所提供的最基础的多线程机制的接口是thread module,这个module是一个builtin module,用C实现。

thread module的基础上,Python提供了一个更高层的多线程机制接口,即threading module,这是一个标准库中的module,用Python实现,为用户提供了更方便的多线程机制接口。

Python线程的创建

在Python的thread module中,提供了start_new_thread方法来创建一个新的线程。

在Python虚拟机启动时,多线程机制并没有被激活,只支持单线程,一旦用户调用start_new_thread,明确指示Python虚拟机创建新的线程,Python就会意识到用户需要多线程的支持。这个时候,Python虚拟机会自动建立多线程机制需要的数据结构/环境以及GIL。

Python线程的调度

1. 标准调度

在标准调度下,在两个切换线程的输出语句之间,只有一个线程的输出结果,要么是主线程的输出结果,要么是子线程的输出结果。因此,标准调度是让一个线程充分执行,直到激发Python的模拟时钟中断,另外一个线程才能有机会执行。

2. 阻塞调度

标准调用是在Python执行完了可执行的指令条数之后才发生的,但是在实际中,Python需要支持另外一种出发机制线程调度的情形,称为阻塞调度。其基本思想是:在线程A通过某种操作,比如等待输入,将自身阻塞后,Python应该将等待GIL的线程B唤醒。

Python子线程的销毁

在线程全部计算完成之后,Python将销毁线程,需要注意的是,Python主线程的销毁与子线程的销毁是不一样的,因为主线程的销毁动作必须要销毁Python的运行时环境,而子线程则不需要这些动作。

Python首先会清理当前线程所对应的线程状态对象,即是对线程状态对象中维护的东西进行引用激素的维护,随后,Python释放GIL。

Python线程的用户级互斥与同步

1. 用户级互斥与同步

Python的线程在GIL控制之下,线程之间,对整个Python解释器,对Python提供的C API的访问,都是互斥的,这可以看作是Python内核级的互斥机制。

内核级的互斥机制是不可控的,因此,我们还需要另外一种互斥机制——用户级互斥机制。

Python线程机制提供了Lock机制来实现用户级互斥。

  • 当主线程通过lock.acquire获得lock之后,将独享input的访问权利。
  • 子线程会因为等待lock而将自身挂起,直到主线程释放lock之后才会被Python的线程调度机制唤醒,获取访问input的权利。
  • 自始至终,每一个线程都能控制自己对input的使用,不用担心别的线程破坏input的状态,这种机制给了用户控制线程之间交互的能力,是Python中实现线程交互和同步的核心。
2. Lock对象

在Python的thread module中,提供了一个thread.allocated()方法来创建一个Lock对象。

Lock对象仅提供了三种操作:acquire/release/locked

一个Python线程在内核级需要访问Python解释器之前,需要先申请GIL,同样的,线程在用户级需要访问共享资源之前也需要先申请用户级的lock,这个申请动作在acquire中完成。

高级线程库——threading

Python中的thread module以及lock对象是Python提供的低级的线程控制库,为了简化多线程应用的开发,Python在thread的基础上构建了一个高级的线程控制库——threading

此处后续给出一个threading库的源码分析。

十六. Python的内存管理机制

内存管理架构

在Python中,有一点是需要特别注意的,Python中所有的内存管理机制都有两套实现,这两套实现由编译符号PYMALLOC_DEBUG控制,当符号被定义时,使用的是debug模式下的内存管理机制,这套机制已正常的内存管理动作之外,还会记录许多有关于内存的信息,以方便Python在开发时进行调试,而此符号未被定义时,Python的内存管理机制只进行正常的内存管理机制。

  • 第一层是Python基于第0层操作系统的内存管理接口包装而成的,这一层并没有在第0层上加入太多的动作,其目的仅仅是为Python提供一层统一的raw memory的管理接口。
    • 有个需要注意的问题,为什么Python会同时提供函数和宏这两套接口?
      • 使用宏可以避免一次函数调用的开销,提高运行效率,但是对于用户使用C来编写Python的扩展模块时,使用宏时危险的,因为随着Python的不断变化,其内存管理机制的具体实现有可能会发生变化,虽然宏的名字不会改变,但是其代表的实现代码有可能会变。
      • 如果使用C来编写Python的扩展模块,使用函数接口时一个良好的编程习惯。
  • 在第二层的内存管理机制上,对于Python中常用对象,比如整数对象,字符串对象等,Python又构建了更高抽象层次的内存管理策略。
  • 对于第三次的内存管理策略,主要就是对象缓冲池机制。

小块空间的内存池

1. Python中的内存池

在Python中,许多时候申请的内存都是小块的内存,这些小块内存在申请后,很快又会被释放,这就意味着Python运行期间会大量的执行malloc和free操作,导致操作系统频发地在用户态和内核态之间进行切换,严重影响效率,基于这种考虑,Python引入了一个内存池机制,用于管理对小块内存的申请和释放。

在Python2.5中,用于管理小块内存池的机制已经是被激活了。

在Python2.5中,整个小块内存的内存池可以视为一个层次结构,在这个层次结构中,从上往下分为四层,分别是:block/pool/arena/内存池

2. Block

在最底层,block是一个确定大小的内存块。

在Python中,有很多种block,不同种类的block有不同的内存大小,这个内存大小的值被称为size class。为了在32位和64位平台上都获得最佳的性能,所有的block的长度都是8字节对齐的。

同时,Python为block的大小设定了一个上限,这个上限值在Python2.5中被设置为256字节,当申请的内存大小小于这个上限时,Python可以使用不同种类的block来满足对内存的需求;当申请的内存大小大于这个上限时,Python将会对内存的请求转交给第一层的内存管理机制来处理。

3. Pool

一组block的集合称为pool,换句话说,一个pool管理着一堆有固定大小的内存块。

在Python中,一个pool的大小通常为一个系统内存页,由于当前大多数Python支持的系统的内存页都是4KB,所以Python内部也将一个pool的大小定义为4KB。

4. Arena

在Python中,多个pool的集合称为arena。

在Python中,每个arena的大小都有一个默认值,在2.5中,这个值由ARENA_SIZE的符号控制,为256KB,那么很显然,一个arena中容纳的pool的个数就是ARENA_SIZE / POOL_SIZE = 64个。

5. 内存池
内存池架构

可用pool缓冲池——usedpools

从上可知,arena是Python小块内存池的最上层结构,所有arena的集合实际就是小块内存,然而在实际的使用中,Python并不直接跟arena打交道,当Python申请内存时,最基本的操作单元不是arena,而是pool。

内存池中的pool,不仅是一个有size概念的内存管理抽象体,而且,更进一步的,它还是一个有状态的内存管理抽象体。

一个pool在Python运行的任何一个时刻,总是处于以下三种状态:

  • used状态:pool中至少有一个block已经被使用,并且至少有一个block还未被使用。这种状态的pool受控于Python受控于Python内部维护的usedpools数组;
  • full状态:pool中所有的block都已经被使用,这种状态的pool在arena中,但不在arena的freepools链表中。
  • empty状态:pool中所有的block都未被使用,处于这个状态的pool的集合通过其pool_header中的nextpool构成一个链表,这个链表的表头就是arena_object中的freepools
Pool的初始化

当Python启动之后,在usedpools这个小块空间内存池中并不存在可用的内存,准确的说,不存在任何可用的pool。在这里,Python采用了延迟分配的策略,即当开始申请小块内存时,Python才开始建立这个内存池。

以申请28个字节的内存为例,当申请28个字节时,Python实际上将申请32字节的内存,Python会先根据32字节对应的class size indexusedpools中对应的位置查找,如果发现在对应的位置后并没有链表任何可用的pool,Python会从useable_arena链表中的第一个可用的arena中获得一个pool。

此时,将这块32字节内存分配的pool放入到usedpools,这一步,叫做init_pool

block的释放

对block的释放实际上是将一块block归还给pool,从上可知,pool可能有3种状态,在分别处于三种状态时,它们各自的位置是不同的。

当释放一个block后,可能会引起pool状态的转变,这种转变分为两种情况:

  • used状态转变为empty状态
  • full状态转变为used状态

当处理完pool之后,要开始处理arena了。对arena的处理分为了4种情况:

  • 如果arena中所有的pool都是empty的,释放pool集合所占用的内存。
  • 如果之前arena中没有了empty的pool,那么在usable_arena链表中就找不到该arena,由于现在arena中有了一个pool,所以需要将这个arena链入到usable_arena链表的表头。
  • 如果arena中的empty的pool个数为n,则从usable_arena链表中开始查找arena可以插入的位置,将arena插入到usable_arena链表中。
  • 其他情况,不进行arena的处理。

循环引用的垃圾收集

1. 引用计数与垃圾收集

引用计数机制非常简单,当一个对象的引用被创建或复制时,对象的引用计数加1,当一个对象的引用被销毁时,对象的引用计数减1,如果对象的引用计数减少为0,那么意味着对象已经不会被任何人使用,可将其占有的内存释放。

引用计数有个缺陷,如果循环引用会使一组对象的引用计数都不为0,然而这些对象实际上并没有被任何外部变量引用,这就意味着不再有人使用这组对象,但是这组对象的所占用的内存永远不会被释放,与内存泄露毫无分别。

2. 三色标记模型

无论哪种垃圾回收机制,一般都分为两个阶段:垃圾检测和垃圾回收。垃圾检测是从所有的已分配的内存中区别可以回收的内存和不可回收的内存,而垃圾回收则是使系统重新掌握在垃圾检测阶段所标记出来的可回收内存块。

标记——清除方法的简要工作如下:

  • 寻找根对象的集合,所谓的根对象即是一些全局引用和函数栈中的引用,这些引用所用的对象是不可被删除的。而这个根对象集合也是垃圾检测动作的起点。
  • 从根对象集合出发,沿着根对象集合中的每一个引用,如果能够到达某个对象A,则A称为可达的,可达的对象也不可被删除。这个阶段称为垃圾检测。
  • 当垃圾检测阶段结束后,所有的对象分为了可达的和不可达的两部分,所有可达的对象都必须予以保留,而所有的不可达的对象所占用的内存将被回收,这就是垃圾回收。

Python中的垃圾收集

在Python中,主要的内存管理手段还是引用计数机制,而标记——删除和分代收集之后为了打破循环引用而引入的补充技术,这一事实意味着Python的垃圾收集只关注可能会产生循环引用的对象。

Python中的循环引用总是发生在container对象之间,所谓container对象即是内部可持有对其他对象的引用的对象,比如list/dict/class/instance等。