今天翻手机备忘录的时候,突然发现了很久之前的一次面试记录,其中的一道面试题,真是让我记忆犹新!因为当年,这道题,直接给我问懵逼了···对话如下
面试官问我:你知道多线程吧,说说看?
我说:当然知道,balabalabala·····
面试官问:嗯,挺好,再解释下你刚才说到那个GIL锁是怎么回事?
我回答:balabalabala···
面试官问:那我问一下,既然已经有了GIL锁控制每个进程只有一个线程执行了,为什么还要给临界资源上锁(threading.Lock())?
我:!!! emm···
一波拟人化的解释python的多线程概念相关别人家的多线程VS自己家的多线程为什么叫假的多线程一句话总结深究一下GIL锁GIL锁对多线程造成的影响了解下GIL锁设计理念解释造成上面代码异常的原因通俗的解释更详细的解释解决问题的方法
下面分三个拟人化的角色先回答当时面试官问我的问题,看不懂的看后面就懂了
对python解释器来说
我(python解释器)是个无感情的工作车间,我手里有一把锁,每次开锁只让一个工人(线程)进来干活,一旦(线程)开始干活,我(解释器)就开始计时(只以时间做举例),假设我只允许每个工人在我这里干15ms,时间一到,马上就把门锁打开,在工人(线程)轰出来,下一个工人进来干活,进来就上锁,只能干15ms就轰走···以此类推
对线程来说
我是个小工(线程),我(线程)进入到工作车间(解释器)开始工作,兢兢业业的反复执行我的代码,本来一个工作流程有4个步骤,突然间,在我执行第二步或者第三步的时候,车间主任(python解释器)直接拿个大棍子就要把我轰走,迫于无奈,我只能放下手头还未完成的工作,出了车间,出了车间后我只能等下次给我排班再继续工作,而此刻别的工人(线程)进入到车间(python解释器)开始工作了,他可不知道你的工作进行到了哪一个步骤,按照她自己的流程开始干活···
对线程锁来说
我(线程)进入到车间开始干活,在工位上,我给我自己(线程)加装个防盗门,整把锁(线程锁),过了一会,虽然车间主任(python解释器)过来告诉我时间到了,但是他(python解释器)没有办法轰我走,因为我有一把自己的锁,所以他(python解释器)只能等我什么时候活干完了,我(线程)自己开锁(线程锁)离开
线程是进程的一个实体,是CPU调度和分配的最小单位。多线程和多进程的区别为,多进程中所需要的资源都是独立的,互不影响,而多线程中则是所有变量共享,而且多线程因为不涉及上下文的切换,所以比多进程要少消耗很多资源,性更高于进程。(这里就简单描述下多线程的概念,毕竟重点不在这)标准意义上的多线程应该可以利用CPU全部的核心去跑每一个线程的,比如JAVA就是满核心跑为什么上面要强调一下标准意义上的多线程的概念?因为只要你是一个pythoner,只要你准备用python构建多线程程序,身边的所有人都会给你讲,python的多线程是假的多线程!为什么这么说?我相信有很多人都能说出来一大堆关于这个问题的答案,就像我面试一样,也基本掌握如何针对CPU密集型的程序写代码,但是我想请问一下,针对这个问题,你真的理解到本质了吗?
对于python解释器来说,无论你有多少的CPU,有多少的核心数,无时无刻,解释器中都只能有一个线程是活动状态,且只使用一个CPU,一个核,其余的线程只能等待解释器释放GIL锁才可以执行,所以称之为假的多线程
先说解释器(我们这里只讨论C语言版本的python), 了解一下,设计者在设计解释器的时候,对python解释器里面限制了一个规则: 所有C语言部分的代码在运行时必须持有锁既然提出了锁的概念,就要构想要基于什么条件实现这个锁。又因为默认python内部对象(比如原子类型的操作,比如列表的排序,你用多少的多线程跑这个排序操作,永远不会排序排一半给你返回结果)操作是thread-safe的,所以不需要考虑额外的内存锁和同步操作,于是乎,最简单的做法就是将锁直接加在了解释器上,大名鼎鼎的GIL锁就诞生了这把锁的作用就是保证无时无刻解释器中都只能有一个线程是活动状态,其余的线程只能等待由于这把锁的存在,我们不能利用计算机多核的特点,永远都只能使用一个核,而内部由于GIL锁切换的非常之快,所以可以达到看似并发的效果,称之为假的多线程
上面应该是解释清楚了为什么python的多线程被称之为假多线程了。那么我相信肯定还有人有疑问,既然只使用了一个核心,那么多线程的意义在哪里呢?还有为什么多线程的效果往往比串行要快太多了,这时候就要来深入的研究下GIL锁了
直接上代码,应该可以很生动的描述了GIL锁容易造成的后果!
# 构造如下的代码,两个线程分别加一执行20万次,然后再减一执行20万次import threading# 加1操作def plus_one(): global n for i in range(200000): n +=1# 减1操作def minus_one(): global n for i in range(200000): n -=1t1=threading.Thread(target=plus_one)t2=threading.Thread(target=minus_one)t1.start()t2.start()t1.join()t2.join()print(n) >>>> 发现并不等于预期结果012345678910111213141516171819202122
我们通过测试可以发现,每次的执行结果都不一致···程序的问题吗?并不是,是GIL锁的问题
当线程休眠或者执行一些IO操作时,立即释放GIL锁,其他线程得以拿到GIL锁执行程序,来回的切换(比如爬虫过程中相关的IO操作,非竞争性)python解释器只检查当前线程所执行的字节码或者时间(读完下面的介绍就知道为啥看这俩了)字节码够了或者时间到了,立即切换,不管当前线程是否执行完成(竞争性)在python2.7版本的时候,GIL锁被设计为每执行完1000个字节则会强制性释放GIL锁,让其他线程得以拿到GIL锁,这个线程也执行1000个字节再释放,下一个线程拿到GIL锁,这样的反复,就使得每个线程都可以快速的切换(来自于python的历史注释)在python3之后,GIL锁的机制则被优化为每15ms则会释放一次GIL锁,其他线程拿到GIL锁,也执行15ms,以此类推,直到多线程结束(来自于python的历史注释)现在最新的python解释器对GIL锁有没有新的优化,我也没去研究,但是不论怎么改,本质是一样的
因为python解释器中正在执行的线程有可能随时中断,释放GIL锁,而别的线程拿到GIL锁之后,有可能取到的值是上一个线程没有计算完成的值,那么就会出现多线程最终的执行的结果出错,当然这种情况一般发生在对于公共资源操作(临界资源)的情况下,如果你还没太懂,可以接下面更细致的解释
你要知道,我们所使用python/java/c等语言被称之为高级语言,比高级语言更接近底层的语言称之为汇编语言,汇编语言再往下就是机器语言了,计算机能读懂的语言就是机器语言了,机器语言再往下就是电信号了然后,我们每一句的高级语言,比如简简单单的一个赋值(一行),翻译成汇编语言都是好几行,主要涉及到的是cpu内部的多个寄存器的状态的变化(具体的可以看王爽老师的’汇编语言’),我这里简单说下
比如说:n += 1这一行代码,对应的汇编语言操作,可能大概就是这样:找到栈空间(这个不用语句,是靠程序一开始系统自动分配的栈空间)1.将n的值赋值给某个寄存器,入栈2.将1也赋值给某个寄存器,入栈3.用特定的寄存器计算,将标识位寄存器记录状态4.返回调用结果,出栈123456
根据这几行的解释,你应该明白了,即便是加一的操作,其实内部也要干这么多的事,那么如果这几步没有全部完成就被解释器排斥出来了,就会对接下来的操作造成一定的影响
# 加一把锁就解决问题了from threading import Lockdef plus_one(l): global n with l: for i in range(200000): n +=1def minus_one(l): global n with l: for i in range(200000): n -=1l=threading.Lock()t1=threading.Thread(target=plus_one, args=(l, ))t2=threading.Thread(target=minus_one, args=(l, ))t1.start()t2.start()t1.join()t2.join()print(n)

被折叠的 条评论
为什么被折叠?



