CPython的内存概念栈、堆和引用

CPython的内存概念栈、堆和引用 Python的实现版本有很多,例如Jython底层就是JVM, IronPython的底层是.Net,它们的内存管理千差万别取决于底层的运行时系统。在CPython实现中,堆和栈有各自的职责。 C语言 堆(heap)和栈(stack)原本是两种不同的数据结构,在C语言内存表述中,代表着用这两种数据结构管理的两种内存块。 堆由整个系统共享,各个进程拥有同一个堆。 栈由每个进程自行管理,也就是每个进程的栈是独立的,互不相关。 具体区别如下: 栈上的内存由系统自动管理分配,用于存储局部变量。 堆中的内存由编程人员主动申请,在C语言中申请内存的函数为malloc, 使用后需要编程人员自行调用free函数释放。 从分配释放及访问速度上,栈内存的存取,申请释放速度要高于堆内存。 栈内存相对于堆内存要小的多,所以在编程的时候,一般不建议使用占空间过大的局部变量。 堆中所有数据均由编程人员申请使用。 栈中除了存放函数中可见的局部变量外,还有各种系统环境数据。 python语言 堆: 主要负责存储CPython运行时的所有对象实体(也就是Python对象的所有属性数据),例如:smt='Hello Word'这个字符串对象PyASCIIObject,n=23这是一个整数PyLongObject,它们都是Python对象,赋值符号=右边的数据值,CPython会将其存储到堆内存中。 栈: 在CPython的语义中,又叫数据栈或值栈,它主要负责保存对堆中Python对象的引用,例如:当CPython在执行smt='Hello Word'这个简单的Python语句,CPython会将'Hello Word'这个字符串实体所处的内存地址压入栈(对于Python语义级别理解,就是对"Hello Word"的引用),而不是将'Hello Word'这个字符串值压入栈。 smt='Hello Word'这些简单的Python赋值语句,你不能单纯地认为将'Hello World'赋值给变量smt,这是大错特错的。 赋值符号右边的是Python对象实体(从C实现的理解,就是构成该PyObject子类对象的属性值,这些值有具体的字面量值表示),并且CPython会为该Python对象在堆中分配内存并且存储它。 而变量smt仅持有该Python对象实体的引用(从C实现的理解,就是该PyObject对象的内存地址),而不是实际的Python对象。 s1变量持有Python对象'Hello world'的引用,对于CPython虚拟机来说,就是在执行s1='Hello Word',将它的内存地址0x71334推入数据栈,那么当CPython碰到同样的语句s2='Hello Word',明显是指向同一个Python对象,那么变量s2和s1一样,它自然持有是'Hello Word'的引用,即s2实质上拥有的'Hello Word'的堆中的地址。 对于其他简易的数据类型,也是如出一辙的。那么现在给Python引用我们可以下一个定义。 Python对象的引用:就是Python变量持有Python对象在堆内存中的内存地址。 我们可以通过python的内置id函数或者关键字is 来判断两个变量是否对同一个对象的引用。 In [1]: s1 = 'hello world' In [2]: s2 = 'hello world' In [3]: id(s1) Out[3]: 4472932144 In [4]: id(s2) Out[4]: 4472034928 在Python中有两种类型的对象:可变对象和不可变对象。 可变对象: 比较典型的就是list,一个列表作为一个对象存储在堆内存中,如果我们要更改该列表的某些元素,它将仍然是内存中的同一个列表对象。 In [5]: alist = [1, 2, ...
2023年10月04日

GIL全局解释器锁

GIL全局解释器锁 GIL:又叫全局解释器锁,每个线程在执行的过程中都需要先获取GIL,保证同一时刻只有一个线程在运行,目的是解决多线程同时竞争解释器资源而出现的线程安全问题。它并不是python语言的特性,仅仅是由于历史的原因在CPython解释器中难以移除,因为python语言运行环境大部分默认在CPython解释器中。 多线程下每个线程在执行的过程中都需要先获取GIL,保证同一时刻只有一个线程在运行。 由于GIL的存在,即使是多线程,事实上同一时刻只能保证一个线程在运行,既然这样多线程的运行效率不就和单线程一样了吗,那为什么还要使用多线程呢? 由于以前的电脑基本都是单核CPU,多线程和单线程几乎看不出差别,可是由于计算机的迅速发展,现在的电脑几乎都是多核CPU了,这时差别就出来了: 即使在多核CPU中,多线程同一时刻也只有一个线程在运行,这样不仅不能利用多核CPU的优势,反而由于每个线程在多个CPU上是交替执行的,导致在不同CPU上切换时造成资源的浪费,反而会更慢。 即原因是一个进程只存在一把gil锁,当在执行多个线程时,内部会争抢gil锁,这会造成当某一个线程没有抢到锁的时候会让cpu等待,进而不能合理利用多核cpu资源。 要去除GIL,主要要考虑四个主要技术点。 引用计数。引用计数在 Python 里是 GCC 的主要方式, C-API 里Py_INCREF 和 Py_DECREF 太好用了,所有C库都用。而这些操作都用到了GIL,只要引用计数活着一天,去掉 GIL 就不容易。 全局和静态变量。这个不用说。 C扩展的并行和重入。 原子性。很多 Python 对象都保证原子性,比如基本类型 list、dict 啥的。 说到在这里要先介绍两个概念:计算密集型和IO密集型 计算密集型:要进行大量的数值计算,例如进行上亿的数字计算、计算圆周率、对视频进行高清解码等等。这种计算密集型任务虽然也可以用多任务完成,但是花费的主要时间在任务切换的时间。这类情况使用多进程实现多任务,可以充分利用多核cpu。 IO密集型:涉及到网络请求(time.sleep())、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。这类情况使用多线程实现多任务,速度要超过单线程的。 解决GIL问题的方案: 使用其他语言的解释器,如java的解释器jython 使用其它语言实现多线程,例如C,Java 使用多进程,是可以利用多核的CPU资源的优势
2023年10月04日