python多线程机制
为了理解Python为什么需要Global Interpreter Lock(GIL),考虑这样的情形:假设有两个线程A、B,在两个线程中,都同时保存着对内存中同一对象obj的引用,也就是说,这时 obj->ob_refcnt的值为2。如果A销毁对obj的引用,显然,A将通过Py_DECREF调整obj的引用计数值。我们知 道,Py_DECREF的整个动作可以分为两个部分:
–obj->ob_refcnt;
if(obj->ob_refcnt == 0) destory object and free memory。
如果A在执行完第一个动作之 后,obj->ob_refcnt的值变为1。不幸的是,恰恰在这个时候,线程调度机制将A挂起,而唤醒了B。更为不幸的是,B同样也开始销毁对 obj的引用。B完成第一个动作之后,obj->ob_refcnt为0,B是一个幸运儿,它没有被线程调度打断,而是顺利地完成了接下来的第二个 动作,将对象销毁,内存释放。好了,现在A又被重新唤醒,可惜现在已经物是人非,obj->ob_refcnt已经被B减少到0,而不是当初的1。 按照约定,傻乎乎的A开始再一次地对已经销毁的对象进行对象销毁和内存释放的动作。这样的结局是什么?只有天知道。
为了支持多线程机制,一个基本的要求就是需要实现不同线程对 共享资源访问的互斥,Python也不例外,这正是引入GIL的根源所在。Python中的GIL是一个非常霸道的互斥实现,正如它的名字所暗示 的,GIL是一个解释器(Interpreter)——为了呼应GIL中的Interpreter,本章中,我们会以解释器来称呼虚拟机——级的互斥机 制,也就是说,在一个线程拥有了解释器的访问权之后,其他的所有线程都必须等待它释放解释器的访问权,即使这些线程的下一条指令并不会互相影响。初看上 去,这样的保护机制粒度太大了,我们似乎只需要将可能被多个线程共享的资源保护起来即可,对于不会被多个线程共享的资源,完全可以不用保护。实际上,在 Python的发展历史中,确实出现过这样的解决方案,但是令人惊奇的,这样的方案在单处理器上的多线程实现的效率上却没有GIL的方案好。所以现在 Python中的多线程机制是在GIL的基础上实现的。
从之前的分析中,我们知道,对于Python而言,字节码解 释器是Python的核心所在,所以Python通过GIL来互斥不同线程对解释器的使用。假如三个拟人化的线程A,B和C都需要使用解释器来执行字节码,以完成某种计算,但是在这之前,它们必须获得GIL,因为GIL把守着通往字节码解释器的大门。当某个线程(A)获得了GIL之后,其他的 两个线程(B,C)只能等待A释放GIL之后,然后才能进入解释器,执行一些计算。实际上,Python的GIL背后所保护的不仅仅是Python的解释 器,同样还有Python的C API,在C/C++和Python的混合开发中,在涉及到原生线程和Python线程的相互协作时,也需要通过GIL进行互斥。关于这一点,我们将在后 面详细阐述。那么A在何时释放GIL呢?如果等到A使用完了解释器之后才释放GIL,这也就意味着,并行的计算退化为了串行的计算,要这样的多线程机制有什么意义呢?所有毫无疑问的,Python拥有一套线程的调度机制。对于线程调度机制而言,同操作系统的进程调度一样,最关键的是要解决两个问题:
在众多的处于等待状态的候选线程中,选择激活哪一个线程?
现在我们知道了,Python控制着什么时候进行线程调度, 当一个线程获得了访问Python解释器的所必须的GIL并进入Python解释器后,Python内部的监测机制就开始启动,当这个线程执行了100条 指令之后,Python解释器将强制挂起当前线程,开始切换到下一个处于等待状态的进程。
那么究竟Python会在众多的等待线程中选择哪一个幸运儿呢?答案是,不知道。没错,对于这个问题,Python完全没有插手,而是交给了底层的操作系统来解决。也就是说,Python借用了底层操作系统所提供 的线程调度机制来决定下一个进入Python解释器的线程究竟是谁。
这一点至关重要,这就意味着Python中的线程实际上就是 操作系统所支持的原生线程,而并非如坊间所流传的那样:Python的线程并非原生线程,而是模拟出来的。Python中的多线程机制正是建立在操作系统 的原生线程的基础之上,对应不同的操作系统,有不同的实现,然而最终,在各不相同的原生线程的基础之上,Python提供了一套统一的抽象机制,给 Python的使用者一个非常简单而方便的多线程工具箱,这就是Python中的两个module:thread以及在其之上的threading。