什么是GIL?
GIL全称global interpreter lock,全局解释器锁。每一个线程在执行之前都需要获取GIL权限才可执行代码,也就是说在多线程中,实际上同一时间只有一个线程在运行。GIL并非是python自带的特性,而是Cpython解释器引入的一个概念,在Jpython(Java实现的python解释器)中就没有GIL。
这是一个历史遗留问题,如今大量开发者习惯了这套机制,代码量也越来越多 ,已经不容易通过修改Cpython来解决这个问题。
并行与并发
在进一步了解GIL之前,我们先回顾一下并行与并发的概念。
并行:同一时间,可以处理多个任务,多个任务是一起被执行。
并发:同一时间,不能处理多个任务,但是可以交替处理多个任务。
举个简单的例子:
你边写作业,边玩手机,说明你支持并行。
你写5分钟作业然后玩10分钟手机,再写5分钟,再玩10分钟,说明你支持并发。
他们虽然方式不同,但都指向了一个点,多任务执行,目的是要提高CPU的使用效率。这里需要注意 一点,单核CPU永远无法实现并行,一个CPU无法同时运行多个程序,但是可以实现并发。在多线程执行代码过程中会带来一个问题,线程间的数据一致性和状态同步完整性。(补充说明:子进程在开始后,他们的执行是随机的,并没有先后顺序)解决这个问题最简单的方案,就是加上GIL这把全局大锁。因为有了GIL的存在,python中多线程的效率会大打折扣。甚至几乎等于python就是单线程程序。
下面我们做一个python下多线程和单线程效率对比:
from threading import Thread
import time
def task(): #执行任务
i = 0
for _ in range(10000000):
i = i + 1
return True
复制代码
下面main1模仿的是串行执行任务,其中采用for循环创建线程主要是增加创建线程的时间,更准确的对比出多线程有GIL的差别。
def main1(): #单线程串行
start_time = time.time()
for tid in range(20):
t = Thread(target=task)
t.start()
t.join()
end_time = time.time()
print("单线程耗时: {}".format(end_time - start_time))
def main2(): #多线程并发
thread_array = {}
start_time = time.time()
for tid in range(20):
t = Thread(target=task)
t.start()
thread_array[tid] = t
for i in range(20):
thread_array[i].join()
end_time = time.time()
print("多线程耗时: {}".format(end_time - start_time))
if __name__ == '__main__':
main1()
main2()
复制代码
测试CPU为i7 7700k,python版本为3.7,测试结果如下:
当任务为计算密集型时,多线程并发对比串行区别并不大。
单线程耗时: 16.951716423034668
多线程耗时: 16.735241651535034
复制代码
下面我们修改一下执行任务,暂停1秒模仿程序IO操作:
def task(): #执行任务
time.sleep(1)
return True
复制代码
执行结果如下:
对比明显,多线程在执行IO密集型效率会比串行高很多。
单线程耗时: 20.019601345062256
多线程耗时: 1.0051548480987549
复制代码
接下来我们对比一下多线程与多进程在计算密集型和IO密集型的差异:
计算密集型
# 计算密集任务
def task():
sum = 1
for i in range(100000000):
sum *= i
pass
复制代码
IO密集型
# IO密集任务
def task():
time.sleep(5)
pass
复制代码
if __name__ == '__main__':
start_time = time.time()
# 多线程
t1 = Thread(target=task)
t2 = Thread(target=task)
t3 = Thread(target=task)
t4 = Thread(target=task)
t5 = Thread(target=task)
t6 = Thread(target=task)
# 多进程
# t1 = Process(target=task)
# t2 = Process(target=task)
# t3 = Process(target=task)
# t4 = Process(target=task)
# t5 = Process(target=task)
# t6 = Process(target=task)
t1.start()
t2.start()
t3.start()
t4.start()
t5.start()
t6.start()
t1.join()
t2.join()
t3.join()
t4.join()
t5.join()
t6.join()
end_time = time.time()
print("多线程耗时: {}".format(end_time - start_time))
复制代码
对比结果如下:
计算密集型
多线程耗时: 38.91398763656616
多进程耗时: 9.934444665908813
复制代码
IO密集型
多线程程耗时: 5.002830743789673
多进程程耗时: 5.4561707973480225
复制代码
在计算密集型中,多进程效率几乎碾压多线程,而在IO密集型多线程的优势就体现出来了,并且随着线程数量越多优势越明显。
总结: 经过实验对比,我们不难发现在GIL下的多线程也有自己的特色,在IO场景下有较好的性能,而在其他方面和单线程差异不大,当遇到被迫需要多线程的场景下,也可以考虑用多进程来代替。Python GIL是功能和性能之间权衡后的产物,它尤其存在的合理性,也有较难改变的客观因素。
ps:本文是学习之后的思考与总结,如有不足之处望指点。