其他分享
首页 > 其他分享> > 来谈谈GIL

来谈谈GIL

作者:互联网

GIL

GIL,Golbal Interprter Lock(全局解释器锁)

从底层实现来看,GIL锁是结构为pthreads互斥锁(mutex)和一个条件变量(cond)构造的二进制信号量(semaphore)的一个实例(instance),这种semaphore的结构如下:

struct

locked = 0 					// Lock status
mutex = pthreads_mutex() 	// Lock for the status
cond = pthreads_cond() 		// Used for waiting/wakeup

locked和mutex区别,locked是程序标记位,在程序中标记锁是否被使用;mutex是pthreads互斥锁,在互斥关系中标记锁确实被使用

lock release

release() {
 mutex.acquire()
 locked = 0
 mutex.release()
 cond.signal()			// send release signal
}

locak acquire

acquire() {
 mutex.acquire()
 while (locked) {
 cond.wait(mutex)		// wait 下面有讲wait中主要运行原理
 }
 locked = 1
 mutex.release()
}

下面先设想一种很简单的切换情况,两个线程,其中一个正在运行时遇到了

I/O操作

image-20210508171803537

从上图可以看出,此时Thread1遇到了I/O操作,随后释放了GIL锁,并发送signal,pthreads/OS也就是操作系统层面与pthread库进行接收,随后切换Thread2的上下文,并且进行GIL获取操作

非I/O操作

当然上面是最简单的情况,我们假设线程1直接遇到了I/O操作,但是如果线程1没有遇到I/O操作,只是正常的调度,那么是如何实现的?

image-20210508173446527

如上图所示,如果是上一个例子遇到I/O时,线程1就不用CPU了,自然只能把锁给Thread2;但是目前这种情况,调度时间到了释放了锁,那么这两个线程该怎样去争夺这个锁?其实奥秘在下图所示:

image-20210508173047090

从上图可以看出,cond在内部维护了一个队列,当收到wait指令时则放入内部队列(通常是FIFO)最后位置,当收到signal信号时(此时mutex已经释放过了),将队列第一个thread弹出

highest priority wins

NEW GIL

python3.2之后,使用了最新的GIL,下面来看下做出了哪些优化

非I/O调度 .p.s:堵塞耗时I/O操作等与3.2之前版本并没有太大差异,就不再列举

3.2版本之前,掌握GIL的线程会以每100ticks为一段定时调度周期的进行调度,到达调度周期后,就会进入check流程,释放GIL锁,并由pthread/OS重新判定GIL归属权;

image-20210508194945579

3.2版本后如上图,掌握GIL的线程永远不会主动的释放GIL(I/O时、或任何堵塞操作时当然是会主动释放的),而Thread2会进行一段时间的等待(默认时间是5ms),超出等待时间线程一仍然没有因为某些耗时而释放GIL,则会主动发送gil_drop_request=1指令给Thread1,Thread1接收到指令时,发送signal信号并挂起,Thread2收到Thread1的signal,返回signal ack确认收到释放信号,类似于TCP握手的思想,如此两边就都确认了GIL的归属,步骤如下图所示

image-20210508200045204

当Thread2收到signal时,就获得了GIL锁。

旧GIL本身的设计存在的问题,在于多线程争夺GIL时有大量的资源消耗,此版本使用等待策略改进了原有的定时释放策略,较大的优化了多计算密集型线程时,耗时比单线程还要大的多的问题,当然并不是解决了这个问题,此种情况时,新的GIL耗时还是会比单线程要多,只不过比旧的GIL要耗时少的多,原因下节会解释。

GIL的缺点所在?

GIL 价值所在?

GIL的宿主CPython解释器,直面的是Python程序的bytecode,CPU每次单独处理一个Thread,保证了bytecode层面的线程安全以及bytecode的原子性,当然了其实对编程时作用不是特别大,因为往往基于bytecode的线程切换并不能保证程序层的线程安全,由于bytecode粒度往往太细,所以一般的python编程还是需要额外的线程锁。

那么GIL存在的价值在哪里?上面说了GIL保证了bytecode的原子性也就是线程安全,确没有阐述这具体有什么用,下面来简单说下。

CPython中一个很人性化的设计是自动化垃圾回收(GC-Garbage collection),其实python的GC整体实现主要依赖引用计数标记清除分代回收,其中主要一部分内容就是依赖于引用计数算法,结合标记清楚和分代回收这两个GC的优化算法,绝大部分情况下能够保证内存空间的正常释放;但引用计数的缺点在于:

  ① 无法释放循环引用的对象

  ② 必须在引用发生增减时对引用计数做出正确的增减

  ③ 引用计数管理并不适合并行处理(其实很好理解,并行的计数肯定会产生冲突)

第一点一般是由标记清除来做优化;第二点保证引用计数加减的准确性就不深入了;唯独第三点,因为引用计数不能做并行处理,当一个Python线程在运行时,它会获取GIL以保证它对对象引用计数的更新是全局同步的,保证引用计数值的准确,而此时引用计数函数获取到GIL时,一定是已经保证了原子性的。

上述来看,GIL的存在虽然被大多数人诟病,并有很多大牛建议以及计划去移除;但由于GIL的存在保证了bytecode的原子性,所以是否取消GIL这个想法到今天依然备受争论。

个人理解下,目前CPython解释器GIL会造成多线程状况下的一些弊端,比如当有I/O非堵塞状况或者CPU密集线程比较多的状况等,一个解决方案是可以利用多进程模块代替。

.P.S:程序是死的人是活的,解决方案是人为决定的;而解决方案的层次是能力决定的。⛵

参考文章:

Understanding the Python GIL by David Beazley

标签:signal,谈谈,mutex,Thread2,线程,GIL,CPU
来源: https://www.cnblogs.com/seasen/p/14754660.html