什么是GIL
GIL的全称是:Global Interpreter Lock,意思就是全局解释器锁,这个GIL并不是python的特性,他是只在Cpython解释器里引入的一个概念,而在其他的语言编写的解释器里就没有这个GIL例如:Jython,Pypy。
GIL是干嘛的
为解决线程之间数据的一致性和状态同步,设计了gil全局解释器锁。例如在多线程中共享全局变量的时候会有线程对全局变量进行的资源竞争,会对全局变量的修改产生不是我们想要的结果。一个广为流传的测试代码:
import threading
global_num = 0
def test1():
global global_num
for i in range(1000000):
global_num += 1
print("test1", global_num)
def test2():
global global_num
for i in range(1000000):
global_num += 1
print("test2", global_num)
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
t2.start()
————————————————
版权声明:本文为CSDN博主「lafeilong」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/feilzhang/article/details/80294572
这段代码里,我们搞了两个线程,每个线程运行一个方法,test1 是累加100000次,test2是累减100000次,按照代码上看,应该结果为0,但是实际运行起来,并不是0,而且每次都不一样。为什么会出现这样的情况。
我们来分析一下实际的运行过程
1.thread1拿到全局变量global_num
2.thread1申请到python解释器的gil
3.解释器调用系统原生线程
4.在cpu1上执行规定的时间
5.执行时间到了,要求释放gil等下一次得到gil的时候,程序从这里接着这一次开始执行
6.thread2拿到了全局变量,此时thread1对全局global_num的操作并未完成,所以thread拿到的和thread拿到的global_num其实是相同的,这样也很好解释为什么结果不是0, 而是其他莫名的数
7.thread2申请到了gil锁
8.调用原生的线程
9…执行时间到了,要求释放gil等下一次得到gil的时候,程序从这里接着这一次开始执行
10.线程1又申请到了gil锁,重复之前的操作。
所以,因为有GIL的存在,会因为一些因素造成释放解释器线程,从而使我们预想的结果与实际结果有偏差,这样的因素大致有两种
- 时间片和代码行数
- io操作
为了控制这种线程释放和获取,产生了互斥锁
什么是线程互斥锁
保证每次对全局变量进行操作的时候,只有一个线程能够拿到这个全局变量;看下面的代码:
import threading
import time
global_num = 0
lock = threading.Lock()
def test1():
global global_num
lock.acquire()
for i in range(1000000):
global_num += 1
lock.release()
print("test1", global_num)
def test2():
global global_num
lock.acquire()
for i in range(1000000):
global_num -= 1
lock.release()
print("test2", global_num)
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
start_time = time.time()
t1.start()
t2.start()
————————————————
版权声明:本文为CSDN博主「lafeilong」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/feilzhang/article/details/80294572
这样,我们在运行的时候就能得到我们想要的结果,0
在Cpython解释器中,当我们的python代码有一个线程开始访问解释器的时候,GIL会把这个大锁给锁上,此时此刻其他的线程只能干等着,无法对解释器的资源进行访问,这一点就跟我们的互斥锁相似。而只是这个过程发生在我们的Cpython中,同时也需要等这个线程分配的时间到了,这个线程把gil释放掉,类似我们互斥锁的lock.release()一样,另外的线程才开始跑起来,说白了,这无疑也是一个单线程。
GIL和互斥锁的区别
全局解释器锁GIL是Python解释器层面的锁,解决解释器中多个线程竞争资源的问题
线程互斥锁是代码层面的锁,解决Python程序中多线程竞争资源的问题
GIL的作用
GIL 对程序中线程的影响,简单来说就是“一个线程运行 Python ,而其他 N 个睡眠或者等待 I/O.”(即保证同一时刻只有一个线程对共享资源进行存取) Python 线程也可以等待threading.Lock或者线程模块中的其他同步对象;线程处于这种状态也称之为”睡眠“。
一个线程无论何时开始睡眠或等待网络 I/O,其他线程总有机会获取 GIL 执行 Python 代码。这是协同式多任务处理。CPython 还有抢占式多任务处理。
当一项任务启动,而在较长的或不确定的时间内,没有运行任何 Python 代码的需要,一个线程便会让出GIL,从而其他线程可以获取 GIL 而运行 Python。这种礼貌行为称为协同式多任务处理,它允许并发;多个线程同时等待不同事件。在同样的时间内它们可以做更多的工作。
Python 的程序分两个阶段运行
首先,Python文件被编译成一个字节码的简单二进制格式。
其次,Python解释器的主回路,一个名叫 pyeval_evalframeex() 的函数,流畅地读取字节码,逐个执行其中的指令。
当解释器通过字节码时,它会定期放弃GIL,而不需要经过正在执行代码的线程允许,这样其他线程便能运行。
- python线程可以主动释放或抓取GIL
随后加上一位博主的总结:
- Python的GIL在单核情况下对性能的影响可以忽略不计,几乎没有。
- Python由于其GIL的存在,在多核CPU的情况下Thread的表现真的是非常的糟糕,但是Process则不受GIL的影响。
- Python内置的数据类是不适合用于大量的数学计算的,当然这也不仅仅是Python的问题,其它完全面向对象的语言都有这个问题, 要进行大量的数学计算就要把代码移到C/C++中去实现,这样不仅可以去除GIL的影响,更可以让性能获得几十倍上百倍的提升, 或者用numpy之类的扩展库在执行科学计算时也可以让性能大幅的提升。
- Python慢其实就是慢在数字计算上,想想就知道,如果每一个数字都是一个对象, 在计算的时候就免不了不断的为对象申请内存,释放内存,速度肯定就慢下来。\
- Python对数据结构的操作是非常高效的,像Python内置的强大的dict,str,list等类型,其处理的速度真的可以和C媲美,因为它们的实现本身就是用C实现的。 我们在编程刚入门的时候就被告知:数据结构+算法=程序,这个道理也许只会在用Python这样的语言时才会有更切身的体会。
- 在用Python开发程序时,你不得不花点时间放在性能优化上来, 过程也很简单:用cProfile类查找出比较耗时的操作,然后将其移到C中去实现, 另外,如果是使用多核CPU的情况,一定要小心使用Thread,尽量用Process来替代Thread,通过本文对GIL的分析,将对性能的优化提供很好的帮助。 其实,Python的性能优化过程也是程序开发中有挑战又非常有成就感的部分。
- 但是,记住一点,不要过早的对程序进行优化,过早优化是罪恶之源 —Donald Knuth。前期开发应该把注意力放在功能实现以及代码的可读性和可维护性上来。
————————————————
版权声明:本文为CSDN博主「SQA_STAR」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/SQA_STAR/article/details/81037781