python 中的 GIL 锁和互斥锁问题
互斥锁的引入
在明确问题前,我们应该知道在python中,线程是并发的而不是并行的,在平时不会有显现,但在资源调度上,这个问题就会特别明显,首先,我们通过一个例子来看一下资源竞争导致的问题:
在这里我们没有采用join方法来使主线程结束在子线程之后,而是采用了一个while循环,当然在本次的验证条件中,这样处理是无关紧要的,下面让我们来看看输出结果:
---线程创建之前g_num is 0--- ----in work1, g_num is 1088005--- ----in work2, g_num is 1286202--- 2个线程对同一个全局变量操作之后的最终结果是:1286202
好吧,这和我们预先的并不一样,我们预想的值应该是2000000,而不是上面显示的输出结果,这就是资源竞争产生的问题。
互斥锁的使用
下面我们通过互斥锁解决上面的问题,我们直接上代码:
让我们看看加上互斥锁后,我们的输出结果会有什么样的改变
---test1---g_num=1909909 ---test2---g_num=2000000 2个线程对同一个全局变量操作之后的最终结果是:2000000
GIL锁的引入
GIL是什么?GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。
每个CPU在同一时间只能执行一个线程(在单核CPU下的多线程其实都只是并发,不是并行,并发和并行从宏观上来讲都是同时处理多路请求的概念。但并发和并行又有区别,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。)
在Python多线程下,每个线程的执行方式:
1.获取GIL
2.执行代码直到sleep或者是python虚拟机将其挂起。
3.释放GIL
可见,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。
GIL锁问题
在真正理解这个问题前,我们仍然通过两端程序了解一下,这个问题究竟会对我们的程序产生怎么样的影响:
上述代码中,我们虽然创建了多线程,但实际上是单线程执行的程序,我们通过将程序使用join方法实现顺序执行,下面我们看看这程序的输出如何?
Total time: 11.4724829197
下面我们再来看看通过多线程实现,程序的效率如何呢?
让我们看一下输出:
Total time: 16.1935360432
对于我们来讲,通过多线程增加程序效率好像是一种常用的手段,但根据上述程序,我们发现多线程不仅不能提升效率反而比我们顺序执行更加缓慢,这是什么原因呢?
问题产生原因
基于pcode数量的调度方式
按照Python社区的想法,操作系统本身的线程调度已经非常成熟稳定了,没有必要自己搞一套。所以Python的线程就是C语言的一个pthread,并通过操作系统调度算法进行调度(例如linux是CFS)。为了让各个线程能够平均利用CPU时间,python会计算当前已执行的微代码数量,达到一定阈值后就强制释放GIL。而这时也会触发一次操作系统的线程调度(当然是否真正进行上下文切换由操作系统自主决定)。
伪代码:
这种模式在只有一个CPU核心的情况下毫无问题。任何一个线程被唤起时都能成功获得到GIL(因为只有释放了GIL才会引发线程调度)。但当CPU有多个核心的时候,问题就来了。从伪代码可以看到,从release GIL到acquire GIL之间几乎是没有间隙的。所以当其他在其他核心上的线程被唤醒时,大部分情况下主线程已经又再一次获取到GIL了。这个时候被唤醒执行的线程只能白白的浪费CPU时间,看着另一个线程拿着GIL欢快的执行着。然后达到切换时间后进入待调度状态,再被唤醒,再等待,以此往复恶性循环。
所以实际上不论我们开设了多少线程,实际上在同一时刻只能有一个在执行。
评论