global interpreter lock
什么是线程、进程?
线程是计算机操作系统进行计算和调度的最小的单位,我们可以简单的理解为,我们的程序都是运行在线程中的,每一个线程都有自己的上下文。
进程是比线程更大一点的单位(资源分配的最小单位),每个进程都有自己的内存之类的,一个进程可以有好几个线程,这些线程会共享这个进程拥有的内存。
这些线程都可以都写同样的变量。
当一个进程有多个线程的时候,就会出现一种情况,叫做racing(竞争冒险),因为一个进程中的所有的线程他们可能同时运行,也有可能交替运行,但不管是同时运行还是交替运行,没有办法控制他们之间的相对顺序,
a = 1
if a > 0:
a -= 1
两个线程同时运行上面的程序,共享a这个变量。由于线程之间的运行相对顺序不可控,我们将其叫做竞争冒险。
在C或者C++中,我们需要显示的去分配内存和释放内存,所以当我们只是显示的分配内存,不释放内存,最终我们的内存会泄露(爆炸)
python 并不需要显示的指定内存和释放内存,所有的python object、list、dictionary这种直接用就好了,是因为python解释器的memory management帮助我们做了这件事,python 是怎样做到自动分配的那,尤其是释放内存的那?
分配内存比较容易,我们什么时候需要我们什么时候申请分配即可;但是我们什么时候释放掉内存那?
python 使用的机制是reference count 引用计数,原理并不难理解,就是每一个python 的object对象都会数着有多少个地方用到了自己,每有一个新的地方用到了自己,就将自己的引用计数 + 1,这个地方不用了,或者这个object没有了,我门的引用计数就-1,这样只要是我们的引用计数数的是对的,我们就能知道我们的引用计数什么时候到0了,没人需要了,那么我就可以将这块内存释放掉了,
以下是cpython的代码:
这个ob_refcnt就是这个PyObject的引用计数保存的地方。
这个_Py_DECREF函数,就是将ob_refcnt减1,直到ob_refcent到了0的时候,就直接将这个object直接deallocate掉
重点理解
上面的事情其实并不难理解,我们结合起竞争冒险的事情,例:我们的一个进程中有多个线程在运行的话,这里就会有一个竞争冒险的事情,因为这个--op->ob_refcnt ,这个动作并不是atomic的,这个--虽然在c里面看起来像是一个操作符,但是他要把这个refcnt的信息读出来,-1在存回去,在这个过程中,就有可能有其他的线程过来,在我们存回去之前,又对refcnt进行修改(操作),这样我们的计数机制就会乱套,可能导致object的引用次数增多,不能保证我们的python object都可以被正确的释放,最终导致内存泄露的问题;或者可能导致减少,甚至实际object的引用次数不是0,错误操作为0,进而导致object被销毁,出现严重程序逻辑错误的问题。
atomic的意思就是,这个线程的程序在运行的过程中,不会被其他的线程打断
在线程中怎样来解决这个问题那,就是加锁
a = 1
lock.acquire()
if a > 0:
a -= 1
lock.release()
加锁的意思就是我的这一段程序只有我一个程序在运行,其他的线程不可以进入这段程序,
通过锁的机制我们可以解决竞争冒险的问题
上面的refcnt的代码中我们可以在外边直接加上一个锁,我们就能解决对于refcnt的竞争冒险的问题。
在python中不光refcnt有这个问题,凡是python中和object有关的代码都会有这个问题,都有可能会有若干个线程会去进行竞争,同时尝试去读或者写这个python object的数据。所以当时设计python的设计者们决定给python设计一个全局的锁,也就是我们所谓的GIL.
这个代码的作用就是拿到GIL
深入进去我们可以看到,收到drop信息,他会现drop掉gil锁,然后在takeGIL锁,通过这个函数可以保证在他运行完之后,当前的线程就会拿到GIL锁,那通过这种机制,python可以保证每一个bytecode在运行的时候,都是拿到线程锁的,换言之,没有bytecode可以被其他的线程打断,这样的话,你在每一个bytecode里面运行的c程序,就都是线程安全的。
GIL全局锁的好处
多核不存在的年代,多线程的存在的意义就是交替执行,不会因为某一个线程计算量很大,就卡住不动的问题,切片并行的理念,一个cpu,本来就只能运行一个线程的代码,所以gil没啥影响
- 非常简单的设计,全局锁相对于每一个object都实现一个自己的锁,要简单非常的多
- 由于只有一个线程锁,避免了线程死锁的问题
- 对于单线程程序和没法并行的多线程的程序,这种全局锁的性能是非常优秀的,全局锁就保证了每一次运行一个bytecode的时候,至多只需要要一次锁,假设一个bytecode中需要access多个object,每个object都有自己的锁的话,加减锁都是需要耗费资源和时间的,程序性能下降
- 在python的代码中写c extension的时候方便了很多,bytecode运行没有竞争冒险的问题,在c代码中修改python object的时候就不用去管锁的问题,让第三方编程人员的开发变得简单了。
GIL全局锁的坏处
- 多核年代,多线程同时进行计算,一个进程的多个线程,可以在多个cpu的核上一起跑,通过并行来增加程序运行速度,一共计算100个数,4个cpu, 每个计算25个数,时间缩短到原来四分之一,这个时候gil锁成了阻碍,因为一个interpreter只能允许一个线程运行它的bytecode,不管多少线程,实际上只有一个线程在运行python代码,这就导致了python 的多线程没办法利用多核来增加运算速度。
- 解决方式
- 使用多进程的方式来利用多个cpu来给程序进行加速。
- 使用c extension扩展写c代码
- 使用没有gil锁的解释器Jython 或者 Ironpython,但是pypy解释器是有gil锁的。