本站分享:AI、大数据、数据分析师培训认证考试,包括:Python培训Excel培训Matlab培训SPSS培训SAS培训R语言培训Hadoop培训Amos培训Stata培训Eviews培训

python多线程机制_python 多线程并发_python培训

python培训 cdadata 2933℃

python多线程机制

关键词:python 多线程python 多线程并发python多线程编程python 多线程爬虫
Python中的线程从一开始就是操作系统的原生线程,而Python虚拟机也同样使用一个全局解释器锁(Global Interpreter Lock,GIL)来互斥线程对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所提供的API。注意这里的同一时间对于单处理器是毫无意义的,因为单处理器的本质是不可能并行的,但是对于多处理器,情形就完 全不同了,同一时间,确实可以有多个线程独立运行,然而Python的GIL限制了这样的情形,使得多处理器最终退化为单处理器,性能大打折扣。这一点其 实早已被Python社区所认识,也进行了大量的探索。大约在99年的时候,Greg Stein和Mark Hammond两位老兄基于Python 1.5创建了一份去除GIL的branch,但是很不幸,这个分支在很多基准测试上,尤其是单线程操作的测试上,效率只有使用GIL的Python的一半 左右。因为细粒度的锁机制会导致大量的加锁、解锁的操作,而加锁、解锁对于操作系统来说,是一个比较重量级的动作;另一方面,没有了GIL的保护,编写 Python扩展模块的难度大大增加。所以,到目前Python的最新版本2.5为止,GIL仍然是多线程机制的基石,而我们也仍然将视线集中在单处理器 上。实际上,在去年5月份Python 3000的邮件列表上,Python的创造者,Guido,提出了一个比较可行的的解决方案,在多处理器的情况下,完全可以创建多个Python进程,充 分使用多处理器,进程之间通过IPC的方式进行通信。当然,Guido也仅仅是提出了这么一个想法,并没有太多的实现细节透露出来。
从之前的分析中,我们知道,对于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自身决定的。考虑一下操作系统是如何进行进程的切换的,当一个进程执行了一段时间之后,发生了时 钟中断,操作系统响应时钟中断,并在这时开始进行进程的调度。同样,Python中也是通过软件模拟了这样的时钟中断,来激活线程的调度。我们知 道,Python字节码解释器的工作原理是按照指令的顺序一条一条地顺序执行,Python内部维护着一个数值,这个数值就是Python内部的时钟,如 果这个数值为N,则意味着Python在执行了N条指令以后应该立即启动线程调度机制
在当前的2.5 中,Python的默认行为是在执行了100条指令以后启动线程调度机制。实际上,这个值不仅仅是用来进行线程调度的,在内部,Python也使用它来检 查是否有异步的事件(event)发生,需要处理。我们可以通过sys.setc- heckinterval()来调节这个值。

现在我们知道了,Python控制着什么时候进行线程调度, 当一个线程获得了访问Python解释器的所必须的GIL并进入Python解释器后,Python内部的监测机制就开始启动,当这个线程执行了100条 指令之后,Python解释器将强制挂起当前线程,开始切换到下一个处于等待状态的进程。

那么究竟Python会在众多的等待线程中选择哪一个幸运儿呢?答案是,不知道。没错,对于这个问题,Python完全没有插手,而是交给了底层的操作系统来解决。也就是说,Python借用了底层操作系统所提供 的线程调度机制来决定下一个进入Python解释器的线程究竟是谁。

这一点至关重要,这就意味着Python中的线程实际上就是 操作系统所支持的原生线程,而并非如坊间所流传的那样:Python的线程并非原生线程,而是模拟出来的。Python中的多线程机制正是建立在操作系统 的原生线程的基础之上,对应不同的操作系统,有不同的实现,然而最终,在各不相同的原生线程的基础之上,Python提供了一套统一的抽象机制,给 Python的使用者一个非常简单而方便的多线程工具箱,这就是Python中的两个module:thread以及在其之上的threading。

转载请注明:数据分析 » python多线程机制_python 多线程并发_python培训

喜欢 (0)or分享 (0)