写点什么

python 中的 GIL 锁和互斥锁问题

用户头像
半面人
关注
发布于: 2020 年 05 月 04 日
python中的GIL锁和互斥锁问题

互斥锁的引入

在明确问题前,我们应该知道在python中,线程是并发的而不是并行的,在平时不会有显现,但在资源调度上,这个问题就会特别明显,首先,我们通过一个例子来看一下资源竞争导致的问题:

import threading
import time
g_num = 0
def work1(num):
global g_num
for i in range(num):
g_num += 1
print("----in work1, g_num is %d---"%g_num)
def work2(num):
global g_num
for i in range(num):
g_num += 1
print("----in work2, g_num is %d---"%g_num)
print("---线程创建之前g_num is %d---"%g_num)
t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()
t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()
while len(threading.enumerate()) != 1:
time.sleep(1)
print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)

在这里我们没有采用join方法来使主线程结束在子线程之后,而是采用了一个while循环,当然在本次的验证条件中,这样处理是无关紧要的,下面让我们来看看输出结果:

---线程创建之前g_num is 0--- ----in work1, g_num is 1088005--- ----in work2, g_num is 1286202--- 2个线程对同一个全局变量操作之后的最终结果是:1286202

好吧,这和我们预先的并不一样,我们预想的值应该是2000000,而不是上面显示的输出结果,这就是资源竞争产生的问题。

互斥锁的使用

下面我们通过互斥锁解决上面的问题,我们直接上代码:

import threading
import time
g_num = 0
def test1(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 解锁
print("---test1---g_num=%d"%g_num)
def test2(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 解锁
print("---test2---g_num=%d"%g_num)
# 创建一个互斥锁
# 默认是未上锁的状态
mutex = threading.Lock()
# 创建2个线程,让他们各自对g_num加1000000次
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()
p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()
# 等待计算完成
while len(threading.enumerate()) != 1:
time.sleep(1)
print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)

让我们看看加上互斥锁后,我们的输出结果会有什么样的改变

---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锁问题

在真正理解这个问题前,我们仍然通过两端程序了解一下,这个问题究竟会对我们的程序产生怎么样的影响:

from threading import Thread
import time
def my_counter():
i = 0
for _ in range(100000000):
i = i + 1
return True
def main():
thread_array = {}
start_time = time.time()
for tid in range(2):
t = Thread(target=my_counter)
t.start()
t.join()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()

上述代码中,我们虽然创建了多线程,但实际上是单线程执行的程序,我们通过将程序使用join方法实现顺序执行,下面我们看看这程序的输出如何?

Total time: 11.4724829197

下面我们再来看看通过多线程实现,程序的效率如何呢?

from threading import Thread
import time
def my_counter():
i = 0
for _ in range(100000000):
i = i + 1
return True
def main():
thread_array = {}
start_time = time.time()
for tid in range(2):
t = Thread(target=my_counter)
t.start()
thread_array[tid] = t
for i in range(2):
thread_array[i].join()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()

让我们看一下输出:

Total time: 16.1935360432

对于我们来讲,通过多线程增加程序效率好像是一种常用的手段,但根据上述程序,我们发现多线程不仅不能提升效率反而比我们顺序执行更加缓慢,这是什么原因呢?

问题产生原因

基于pcode数量的调度方式



按照Python社区的想法,操作系统本身的线程调度已经非常成熟稳定了,没有必要自己搞一套。所以Python的线程就是C语言的一个pthread,并通过操作系统调度算法进行调度(例如linux是CFS)。为了让各个线程能够平均利用CPU时间,python会计算当前已执行的微代码数量,达到一定阈值后就强制释放GIL。而这时也会触发一次操作系统的线程调度(当然是否真正进行上下文切换由操作系统自主决定)。

伪代码:

while True:
acquire GIL
for i in 1000:
do something
release GIL
/* Give Operating System a chance to do thread scheduling */

这种模式在只有一个CPU核心的情况下毫无问题。任何一个线程被唤起时都能成功获得到GIL(因为只有释放了GIL才会引发线程调度)。但当CPU有多个核心的时候,问题就来了。从伪代码可以看到,从release GIL到acquire GIL之间几乎是没有间隙的。所以当其他在其他核心上的线程被唤醒时,大部分情况下主线程已经又再一次获取到GIL了。这个时候被唤醒执行的线程只能白白的浪费CPU时间,看着另一个线程拿着GIL欢快的执行着。然后达到切换时间后进入待调度状态,再被唤醒,再等待,以此往复恶性循环。



所以实际上不论我们开设了多少线程,实际上在同一时刻只能有一个在执行。



用户头像

半面人

关注

还未添加个人签名 2020.04.29 加入

还未添加个人简介

评论

发布
暂无评论
python中的GIL锁和互斥锁问题