GIL (Global Interpreter Lock) 全局解释锁
参考资料
每一个interpreter进程,只能同时仅有一个线程来执行, 获得相关的锁, 存取相关的资源.
那么很容易就会发现,如果一个interpreter进程只能有一个线程来执行, 多线程的并发则成为不可能, 即使这几个线程之间不存在资源的竞争.
- 并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念
- 一个防止多线程并发执行机器码的一个Mutex
- 设计初衷是,解决线程间数据一致性和状态同步的困难
改进:为了让各个线程能够平均利用CPU时间,python会计算当前已执行的微代码数量,达到一定阈值后就强制释放GIL。
而这时也会触发一次操作系统的线程调度(当然是否真正进行上下文切换由操作系统自主决定)。
问题: (前提单进程)
[单核多线程ok,处理io密集型和文件密集型不错]这种模式在只有一个CPU核心的情况下毫无问题。任何一个线程被唤起时都能成功获得到GIL
(因为只有释放了GIL才会引发线程调度)。
[多核单进程不好]但当CPU有多个核心的时候,大部分情况下主线程已经又再一次获取到GIL了。这个时候被唤醒执行的线程只能白白的浪费CPU时间,
看着另一个线程拿着GIL欢快的执行着。然后达到切换时间后进入待调度状态,再被唤醒,再等待,以此往复恶性循环。
总结:Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。
2.1 引用计数RC(reference count) [解决垃圾回收]
-> 垃圾回收GC(garbage collection)
为了解决内存泄露问题,采用了对象引用计数,并基于引用计数实现自动垃圾回收。
优点:
虽然引用计数必须在每次分配和释放内存的时候加入管理引用计数的动作,然而与其他主流的垃圾收集技术相比,
引用计数有一个最大的有点,即“实时性”,任何内存,一旦没有指向它的引用,就会立即被回收。
而其他的垃圾收集计数必须在某种特殊条件下(比如内存分配失败)才能进行无效内存的回收。
缺点
循环引用问题
解决方案:“标记-清除”,“分代回收”两种收集技术。
参考文档
2.1 标记-清除 [解决循环引用]
可以包含其他对象引用的容器对象(比如:list,set,dict,class,instance)都可能产生循环引用。
容器+可变变量
原理:并不改动真实的引用计数,而是将集合中对象的引用计数复制一份副本,在副本上标记清除。
对于副本做任何的改动,都不会影响到对象生命走起的维护。
计数副本的唯一作用是寻找root object集合(该集合中的对象是不能被回收的,根对象集合)。
当成功寻找到root object集合之后,首先将现在的内存链表一分为二,一条链表中维护root object集合,
成为root链表<不被清除的>,而另外一条链表中维护剩下的对象,成为unreachable链表。
处理: 现在的unreachable可能存在被root链表中的对象,直接或间接引用的对象,
这些对象是不能被回收的,一旦在标记的过程中,发现这样的对象,就将其从unreachable链表中移到root链表中;
当完成标记后,unreachable链表中剩下的所有对象就是名副其实的垃圾对象了,
接下来的垃圾回收只需限制在unreachable链表中即可。
2.2 分代回收 [解决垃圾回收,加快回收]
存活时间划分为不同的集合,"代"
背景: 当需要回收的内存块越多时,垃圾检测带来的额外操作就越多,而垃圾回收带来的额外操作就越少。
为了提高垃圾收集的效率,采用“空间换时间的策略”。
原理:将系统中的所有内存块根据其存活时间划分为不同的集合,每一个集合就成为一个“代”,
垃圾收集的频率随着“代”的存活时间的增大而减小。也就是说,活得越长的对象,就越不可能是垃圾,
就应该减少对它的垃圾收集频率。
那么如何来衡量这个存活时间:通常是利用几次垃圾收集动作来衡量,如果一个对象经过的垃圾收集次数越多,
可以得出:该对象存活时间就越长。0代,1代
网友评论