Python中的GIL

一直有一种说法,说python中的多线程是伪多线程,是一种鸡肋。为什么会有这种说法呢?

GIL

GIL的意思是 Global Interpreter Lock,即全局解释器锁,在cpython的解释器中,同一个时刻只允许有一个线程在运行。

这就是说,cpython中的多线程和单线程的效率应该是一致的,因为同一时刻只能有一个线程在工作。事实是这样吗?我们来验证一下

代码1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def plus_to_k(x = 0):
'''
累加到1000
'''
while x<= 1000:
x +=1

def plus_to_k_multi(x=0):
x+=1

def mulit_plus():
with futures.ThreadPoolExecutor(max_workers=2) as executor:
for _ in range(1000):
executor.submit(plus_to_k_multi)

if __name__ == "__main__":
r1 = timeit.timeit(stmt=plus_to_k,number=1)
r2 = timeit.timeit(stmt=mulit_plus,number=1)

---------
4.2396015487611294e-05
0.013870124006643891

从结果看,多线程的效率并没有单线程高,这与我们的推论不符,这是因为mulit_plus函数虽然启动了多个1000个线程,但是因为线程切换和开辟内存空间等操作的耗时,效率并没有比单线程快。
再看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import requests
import time
def req_mx():
for i in range(100):
res = requests.get("http://www.baidu.com")

def req_baidu():
res = requests.get("http://www.baidu.com")

def multi_req_mx():
with futures.ThreadPoolExecutor(max_workers=3) as executor:
for _ in range(10):
executor.submit(req_baidu)

if __name__ == "__main__":
# r1 = timeit.timeit(stmt=req_mx,number=1)
# print(r1)
# print('--------')
# r2 = timeit.timeit(stmt=multi_req_mx,number=1)
# print(r2)

r1 = timeit.timeit(stmt=plus_to_k,number=1)
print(r1)
r2 = timeit.timeit(stmt=mulit_plus,number=1)
print(r2)

------
1.2559407940134406
0.04530800099018961

这次可以看出来,多线程的确实比单线程要快。这又是为什么呢?

这是因为,在标准库和第三方库中,都被设计成了在进行IO密集型的业务时释放GIL,也就是一个线程在等待网络响应的过程中,释放了GIL,其他的线程就可以进行工作,从而加速了这个过程。

Python中的多线程是鸡肋吗

答案是看具体的业务场景:

1、CPU密集型代码(各种循环处理、计数等等),在这种情况下,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好。

2、IO密集型代码(文件处理、网络爬虫等),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。

而在python3.x中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL),这样对CPU密集型程序更加友好,但依然没有解决GIL导致的同一时间只能执行一个线程的问题,所以效率依然不尽如人意。

多核多线程比单核多线程更差,原因是单核下多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够无缝执行,但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低。

对于IO密集型操作,我们应该使用多进程,但是在IO密集型的操作上使用多进程并没有什么好处,因为服务器的CPU核心总是有限的,但是线程却可以开很多个。对于CPU密集型的工作来说,PyPy的速度会更快。