python多线程机制


开发多线程的应用系统,是在日常的软件开发中经常会遇到的需求。现在的编程语言都为多线程开发提供了很好的支持,无论是通过库的支持还是将多线程机制内建在语言之中。Python也为多线程系统的开发提供了很好的支持。

同样身为动态语言,Ruby也提供了多线程的支持,但是在Ruby1.9之前的多线程机制是在语言的实现中模拟了线程及线程调度机制,而并没有使用操作系统本身的线程机制(在以后的描述中,我们称为原生线程)。Ruby1.9中整合了YARV作为Ruby新的虚拟机,在YARV中,将操作系统的原生线程引入了Ruby。每一个Ruby线程都对是操作系统上的一个线程,在Ruby内部,维护着一个全局资源锁,一个Ruby线程必须首先获得这个锁,才能成为活动的线程,从而使用Ruby虚拟机的全局资源。

这一切,在Python中早已实现,Python中的线程从一开始就是操作系统的原生线程,而Python虚拟机也同样使用一个全局解释器锁(Global Interpreter Lock,GIL)来互斥线程对Python虚拟机的使用。

15.1  GIL与线程调度

为了理解Python为什么需要GlobalInterpreterLock(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年的时候,GregStein和Mark Hammond两位老兄基于Python1.5创建了一份去除GIL的branch,但是很不幸,这个分支在很多基准测试上,尤其是单线程操作的测试上,效率只有使用GIL的Python的一半左右。因为细粒度的锁机制会导致大量的加锁、解锁的操作,而加锁、解锁对于操作系统来说,是一个比较重量级的动作;另一方面,没有了GIL的保护,编写Python扩展模块的难度大大增加。所以,到目前Python的最新版本2.5为止,GIL仍然是多线程机制的基石,而我们也仍然将视线集中在单处理器上。实际上,在去年5月份Python3000的邮件列表上,Python的创造者,Guido,提出了一个比较可行的的解决方案,在多处理器的情况下,完全可以创建多个Python进程,充分使用多处理器,进程之间通过IPC的方式进行通信。当然,Guido也仅仅是提出了这么一个想法,并没有太多的实现细节透露出来。

图15-1显示了我们对Python的多线程机制所建立的一个粗略的模型。

python多线程机制 - 苏克儿 - 计算机软件技术与理论研讨

图15-1  Python线程机制的粗略模型

从之前的分析中,我们知道,对于Python而言,字节码解释器是Python的核心所在,所以Python通过GIL来互斥不同线程对解释器的使用。在图15-1中,三个拟人化的线程A,B和C都需要使用解释器来执行字节码,以完成某种计算,但是在这之前,它们必须获得GIL,因为GIL把守着通往字节码解释器的大门。当某个线程(A)获得了GIL之后,其他的两个线程(B,C)只能等待A释放GIL之后,然后才能进入解释器,执行一些计算。实际上,Python的GIL背后所保护的不仅仅是Python的解释器,同样还有Python的CAPI,在C/C++和Python的混合开发中,在涉及到原生线程和Python线程的相互协作时,也需要通过GIL进行互斥。关于这一点,我们将在后面详细阐述。

那么A在何时释放GIL呢?如果等到A使用完了解释器之后才释放GIL,这也就意味着,并行的计算退化为了串行的计算,要这样的多线程机制有什么意义呢?所有毫无疑问的,Python拥有一套线程的调度机制。

对于线程调度机制而言,同操作系统的进程调度一样,最关键的是要解决两个问题:

?       在何时挂起当前线程,选择处于等待状态的下一个线程?

?       在众多的处于等待状态的候选线程中,选择激活哪一个线程?

在Python的多线程机制中,这两个问题是分别由不同的层次解决的。对于何时进行线程调度的问题,是由Python自身决定的。考虑一下操作系统是如何进行进程的切换的,当一个进程执行了一段时间之后,发生了时钟中断,操作系统响应时钟中断,并在这时开始进行进程的调度。同样,Python中也是通过软件模拟了这样的时钟中断,来激活线程的调度。我们知道,Python字节码解释器的工作原理是按照指令的顺序一条一条地顺序执行,Python内部维护着一个数值,这个数值就是Python内部的时钟,如果这个数值为N,则意味着Python在执行了N条指令以后应该立即启动线程调度机制,图15-2显示了如何获得Python内部默认设定的这个值。

python多线程机制 - 苏克儿 - 计算机软件技术与理论研讨

图15-2  Python2.5内部的“时钟中断”间隔值

图15-2显示的结果意味着,在当前的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。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值