18.3 Python、线程和全局解释器锁

18.3.1 全局解释器锁(GIL)

Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行,就像单CPU的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在CPU中运行。同样地,虽然Python解释器中可以“运行”多个线程,但在任意时刻,只有一个线程在解释器中运行。

对Python虚拟机的访问由全局解释器锁(global interpreter lock, GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。在多线程环境中,Python虚拟机按以下方式执行。

1.设置GIL。

2.切换到一个线程去运行。

3.运行:

a.指定数量的字节码的指令,或者

b.线程主动让出控制(可以调用time.sleep(0))。

4.把线程设置为睡眠状态。

5.解锁GIL。

6.再次重复以上所有步骤。

在调用外部代码(如C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)。编写扩展的程序员可以主动解锁GIL。不过,Python的开发人员则不用担心在这些情况下你的Python代码会被锁住。

例如,对所有面向I/O的(会调用内建的操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其他的线程在这个线程等待I/O的时候运行。如果某线程并未使用很多I/O操作,它会在自己的时间片内一直占用处理器(和GIL)。也就是说,I/O密集型的Python程序比计算密集型的程序更能充分利用多线程环境的好处。

对源代码,解释器主循环和GIL感兴趣的人,可以看看Python/ceval.c文件。

18.3.2 退出线程

当一个线程结束计算,它就退出了。线程可以调用thread.exit()之类的退出函数,也可以使用Python退出进程的标准方法,如sys.exit()或抛出一个SystemExit异常等。不过,你不可以直接“杀掉”(kill)一个线程。

在下面一节中,我们将要讨论两个跟线程有关的模块。这两个模块中,我们不建议使用thread模块。这样做有很多原因,很明显的一个原因是,当主线程退出的时候,所有其他线程没有被清除就退出了。但另一个模块threading就能确保所有“重要的”子线程都退出后,进程才会结束。(我们等一会儿会详细说明什么叫“重要的”,请参阅守护线程的“核心提示”)。

主线程应该是一个好的管理者,它要了解每个线程都要做些什么事,线程都需要什么数据和什么参数,以及在线程结束的时候,它们都提供了什么结果。这样,主线程就可以把各个线程的结果组合成一个有意义的最后结果。

18.3.3 在Python中使用线程

在Win32和Linux, Solaris, MacOS, *BSD等大多数类Unix系统上运行时,Python支持多线程编程。Python使用POSIX兼容的线程,即pthreads。

默认情况下,从源代码编译的(2.0及以上版本的)Python以及Win32的安装包里,线程支持是打开的。想要从解释器里判断线程是否可用,只要简单地在交互式解释器里尝试导入thread模块就行了,只要没出现错误就表示线程可用。

18.3 Python、线程和全局解释器锁 - 图1

如果你的Python解释器在编译时,没有打开线程支持,导入模块会失败:

18.3 Python、线程和全局解释器锁 - 图2

这种情况下,你就要重新编译你的Python解释器才能使用线程。你可以在运行配置脚本的时候,加上“-with-thread”参数。参考你的发布版的README文件,以获取如何编译支持线程的Python的相关信息。

18.3.4 没有线程支持的情况

第一个例子中,我们会使用time.sleep()函数来演示线程是怎样工作的。time.sleep()需要一个浮点型的参数,来指定“睡眠”的时间(以秒为单位)。这就意味着,程序的运行会被挂起指定的时间。

我们要创建两个“计时循环”。一个睡眠4秒种,一个睡眠2秒种,分别是loop0()和loopl()。(我们命名为“loop0”和“loop1”表示我们将有一个循环的序列)。如果我们像例18. 1的onethr.py中那样,在一个进程或一个线程中,顺序地执行loop0()和loop1(),那运行的总时间为6秒。在启动loop0(),loop1()和其他的代码时,也要花去一些时间,所以,我们看到的总时间也有可能会是7秒钟。

在单线程中顺序执行两个循环。一个循环结束后,另一个才能开始。总时间是各个循环运行时间之和。

例18.1

18.3 Python、线程和全局解释器锁 - 图3

我们可以通过运行onethr.py来验证这一点,下面是运行的输出:

18.3 Python、线程和全局解释器锁 - 图4

假定loop0()和loop1()里做的不是睡眠,而是各自独立的,不相关的运算,各自的运算结果到最后将会汇总成一个最终的结果。这时,如果能让这些计算并行执行的话,那不是可以减少总的运行时间吗?这就是我们现在要介绍的多线程编程的前提条件。

18.3.5 Python的threading模块

Python提供了几个用于多线程编程的模块,包括thread、threading和Queue等。thread和threading模块允许程序员创建和管理线程。thread模块提供了基本的线程和锁的支持,而threading提供了更高级别,功能更强的线程管理的功能。Queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。我们将分别介绍这几个模块,并给出一些例子和中等大小的应用。

18.3 Python、线程和全局解释器锁 - 图5核心提示:避免使用thread模块

出于以下几点考虑,我们不建议您使用thread模块。首先,更高级别的threading模块更为先进,对线程的支持更为完善,而且使用thread模块里的属性有可能会与threading出现冲突。其次,低级别的thread模块的同步原语很少(实际上只有一个),而threading模块则有很多。

不过,出于对学习Python和线程的兴趣,我们将给出一点使用thread模块的例子。这些代码只用于学习目的,让你对为什么应该避免使用thread模块有更深的认识,以及让你了解在把代码改为使用threading和Queue模块时,我们能获得多大的便利。

另一个不要使用thread原因是,对于你的进程什么时候应该结束完全没有控制,当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作。我们之前说过,至少threading模块能确保重要的子线程退出后进程才退出。

只建议那些有经验的专家在想访问线程的底层结构的时候,才使用thread模块。而使用线程的新手们则应该看看我们是如何把线程应用到我们的第一个程序,从而增加代码的可读性,以及第一段例子如何进化到我们本章的主要的代码的。如果可以的话,你的第一个多线程程序应该尽可能地使用threading等高级别的线程模块。