day9-为什么会有GIL锁
背景
我的机器有4核,意味着在同一时间可以干4个任务。如果单核cpu的话,我启动10个线程,我看上去也是并发的,为什么?是因为执行了上下文的切换,让我们看上去是并发的。但是单核的情况下永远肯定是串行的,cpu真正执行的时候,因为一会儿执行1,一会儿执行2,一会儿执行3.....一会儿又切回到1,就是这样如此反复切换上下文。多核情况下,同时有几个线程并发执行,这是正常的线程执行过程。但是,在python中,无论你有多少核,永远都是假象。无论你是4核,8核,还是16核.......不好意思,同一时间执行的线程只有一个线程,它就是这个样子的。这个是python的一个开发时候,设计的一个缺陷,所以说python中的线程是假线程。
全局解释器锁(GIL)
Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
为什么需要这个GIL锁
因为Python的线程是调用C语言的原生线程。因为Python是用C写的,启动的时候就是调用的C语言的接口。因为启动的C语言的远程线程,那它要调这个线程去执行任务就必须知道上下文,所以Python要去调C语言的接口的线程,必须要把这个上下文关系传给Python,那就变成了一个我在加减的时候要让程序串行才能一次计算。就是先让线程1,再让线程2.......
每个线程在执行的过程中,Python解释器是控制不了的,因为调用的是的C语言的接口,超出了Python的控制范围,python的控制范围是只在Python解释器这一层,所以Python控制不了C语言接口,它只能等结果。所以它不能控制让哪个线程先执行,因为是一块调用的,只要一执行,就是等返回的结果,这个时候4个线程独自执行,所以结果就不一定正确了。有了GIL,实现了在同一时间只有一个线程能够工作。虽然这4个线程都启动了,但是同一时间只能让一个线程拿到这个数据去修改。其他的几个线程都处于等待状态。Python启动的4个线程确确实实打到了这4个cpu上,重复上面的话,同一时间只有一个线程在一个CPU上执行,这就是为了避免数据操作出错。这也是CPython的一个缺陷,其他语言没有,仅仅只是CPython有。
我们这里有num=2,现在需要4个线程对num进行加1的操作,并返回结果,python的执行都是通过python解释器(c语言写的)执行的,且解释器运行在OS上,当程序运行时,python解释器调用c语言的接口通过OS启动4个新的线程,同时OS将程序交给CPU去执行,如果该程序启动了4个线程,那么问题来了?如果python让CPU启动线程和执行任务必须要告诉CPU的上下文关系,所以该程序将启动4个线程都修改数据加1,每个线程都加1,正常情况下,4个线程执行完,返回的结果都为3,本来我们需要的是每个线程加1,应该为6,怎么办呢?应该串行执行,让线程1执行加1后返回结果再让2执行返回后,直到第4线程执行完。这是我们认为的顺序,但是python解释器是直接调用C语言的线程接口,它就像调用一个函数一样,直接运行函数,我们无法对其线程执行顺序进行控制,也就是说num=2都交由4个线程去单独执行。那么可能出现最终结果有可能是3或4或5。当线程1在执行时,2,3,4线程在等待1线程处理完再执行,有可能这4个线程运行在不同的CPU上。
那么,不同线程同时访问时,数据的保护机制是怎样的呢?答案是解释器全局锁。从名字上看能告诉我们很多东西,很显然,这是一个加在解释器上的全局锁。对于任何Python程序,不管有多少的处理器,任何时候都总是只有一个线程在执行。没有这个锁,甚至多线程程序中最简单的操作都会发生问题。为什么无论怎样,GIL需要保证只有一个线程在某一时刻处于运行中?
总结
基于线程的编程毫无疑问是困难的。每当某个人觉得他了解关于线程是如何工作的一切的时候,总是会悄无声息的出现一些新的问题。因为在这方面想要得到正确合理的一致性真的是太难了,因此有一些非常知名的语言设计者和研究者已经总结得出了一些线程模型。就像某个写过多线程应用的人可以告诉你的一样,不管是多线程应用的开发还是调试都会比单线程的应用难上数倍。程序员通常所具有的顺序执行的思维模恰恰就是与并行执行模式不相匹配。GIL的出现无意中帮助了开发者免于陷入困境。GIL事实上帮助我们保持不同线程之间的数据一致性问题。