一、线程互斥锁Lock:
从一段多线程的代码说起:
以上代码表示开启两个线程去执行add_a函数,该函数的功能是对全局变量a不断的累加到1000000.
计算机内存中的数据若想实现被修改,就需要先加载到寄存器中,被cpu运算之后再放回内存中才能完成一次修改操作,对于a += 1这个操作来说,要经历读取内存、加1、返回内存这些步骤.
上面的代码中有两个线程t1和t2(后称线程1和线程2).
当cpu分配给线程1的时间片运行结束时,而cpu对全局变量a的修改只完成到运算(+1)这一步,还没有将+1的结果返回到内存中,那么对于该线程来说,它已经运算了1次,而内存中的全局变量a却还是0。此时,线程2获取时间片,在读取内存中变量a的数据时就读到的就是0而不是1。而线程1运行到加1操作的这个状态被保存到了解释器的内存(该内存也是公有资源)中,假设线程2刚好执行完加1操作且将a=1返回到内存中,则此时内存中的a的值就是1.
当线程2的时间片结束后,假设线程1又被分配了时间片,那么线程1就要从解释器中将之前保存的运行状态取出,即a=1,然后将a=1返回到内存当中去,至此,a被执行了2次,但是值却是1。这就是多线程在操作全局变量时可能会导致数据出错的原因.
要想解决上述问题,可以用线程互斥锁将全局变量a锁住:
线程互斥锁的原理:
当线程1的时间片结束之后,其运行状态会被保存到GIL中,那么如果线程1在操作变量a时对其加锁,那么全局变量a被线程1的线程互斥锁锁住的状态也同样会被保存。当线程1的时间片结束,这时线程2获取到时间片之后去访问全局变量a,发现a是被线程1锁住的,所以线程2无法获取该变量,只能干运行,等其时间片结束之后,线程1重获时间片然后将解释器中保存的线程运行状态取出继续操作全局变量a。这样就避免了多线程中由于时间片分配的问题而导致的全局变量数据出错.
二、全局解释器锁GIL:
但是现在又有一个问题,定义的a是全局变量,在不加线程互斥锁的时候,由于时间片资源的分配会导致a的数据发生异常,那么解释器中的内存资源也是两个线程所共有的,但是我们发现只是添加了线程互斥锁就可以实现全局变量数据的正常计算,而解释器中的共有资源却并没有任何操作,难道在解释器中就不会出现资源的竞争问题吗?
事实上,在CPython解释器中已经自动的存在了一个锁,就是GIL(只有CPython中存在GIL),它存在的意义就是为了解决多线程之间对共有资源竞争的问题。类似线程互斥锁锁住全局变量一样,GIL锁住了解释器中用来保存线程运行状态的全局变量,那么其他的线程就不会修改该变量保存的数据,因此可以保证在解释器中不会出现数据(线程运行状态)的错乱.
在进行多线程编程时,在线程互斥锁和全局解释器锁的双重保护下,我们才得以保证线程的安全,只不过线程互斥锁需要我们手动添加,而GIL是解释器已经实现好了的.
那么GIL锁的存在会对程序产生什么影响?
当计算机在执行计算任务时,需要用到cpu去执行。如果一个计算机有2个cpu,按照"我觉得"的思维方式:线程1和线程2的任务可以分别被cpu1和cpu2同时运算。但是Python中存在的GIL锁会锁住某个线程的运行状态,线程1的时间片在没有结束之前,GIL锁就一直被线程1占用。线程2因为无法抢占GIL锁而不能获取到解释器中的变量(运行状态,如+1操作),也就没有数据可供操作,就像是一个学霸终于获得了答题权(时间片),却发现没有题目(GIL锁)可以答一样,这样看来cpu2貌似就是个摆设。只有当线程1释放了GIL锁之后,线程2抢到GIL锁时才能正常运行,并在cpu2中进行计算操作。同理,此时的线程1也成了摆设,但是由于时间片的时间很短,在我们可感知的时间内,线程1和线程2已经来回的切换了很多次,在视觉的感知上,仿佛两个线程在同时运行一样.
如此看来,不管计算机有多少cpu,也不管Python中开了多少个线程,对于Python来说,由于GIL锁的存在,在同一时刻,只能有1个线程和1个cpu在工作,其他的都是摆设。也许Python这时候就会说,我不要你觉得,我要我觉得,我的GIL锁住哪个线程,其他的线程就别想同时运行.
现在,重新举个例子,如果让线程2去执行一个文件读写任务(IO操作),而线程1去执行一个计算任务(cpu操作),那么在两个线程启动的时候,仍然会产生抢夺GIL锁的情况,只是解释器这个时候会分辨出线程2执行的操作是IO操作,而IO操作是非常耗时的,却不需要cpu去工作,此时解释器就会自动的释放线程2的GIL锁,让线程1抢占,因为cpu的操作效率非常高,因此在线程2执行IO操作的那段时间内,线程1也得到了执行。所以多线程中对IO操作的效率是有所提高的。就好像放炮一样,如果你点燃一个炮,非要等他炸完之后再去点燃下一个,那么点完一盒炮的时间就要等很久。而如果你把炮点燃就扔了,继续点下一个,而不必等待上一个爆炸,那么放炮速度就会很快.
GIL锁何时会被释放:
1、当时间片到期时
2、当遇到IO等待时
3、当执行的字节码达到一定的数量时(默认100)
通过上述内容可以获得以下结论:
Python的多线程并不适合计算密集型任务,而遇到IO密集型任务时会提升一些效率.
领取专属 10元无门槛券
私享最新 技术干货