多线程编程(三) 线程和Python


  • 背景/简介
  • 线程和进程
  • 线程和Python
  • thread 模块
  • threading模块
  • 单线程和多线程对比
  • 多线程实践
  • 生产者-消费者问题和Queue/queue 模块
  • 线程的替代方案

全局解释锁

python代码的执行是由python虚拟机(又称解释器主循环)进行控制的。Python在设计时是这样考虑的,在主循环中同时只能有一个控制线程在执行,就像单核CPU中的多线程一样。内存中可以有许多程序,但是在任意给定时刻只能有一个程序在执行。同理,尽管python解释器中可以运行多个线程,但是在给定任意时刻只能有一个线程会被解释器执行。
对python虚拟机的访问是由全局解释器锁(GIL)控制的。这个锁就是用来保证同时,只能有一个线程运行的。

在多线程环境中。python虚拟机将按照下面所述的方式执行:

     1. 设置GIL

     2. 切换进一个线程取运行

     3 .执行下面操作之一

        a. 指定数量的字节码指令

        b.线程主动让出控制权(可以调用 time.sleep(0) 来完成)  

     4.把线程设置回睡眠状态(切换出线程)

     5.解锁GIL

     6.重复上述步骤

当调用外部代码(即,任意C/C++扩展内置函数)时,GIL会保持锁定,直至函数执行结束(因为在这期间没有 Python 字节码计数)。编写扩展函数的程序员有能力解锁 GIL,然而,作为Python开发者,你并不需要担心Python代码会在这些情况下被锁住。例如,对于任意面向 I/O 的 Python 例程 (调用了内置操作系统 C 代码的那种)。

GIL会在I/O 调用前被释放,以允许其他线程在 I/O 执行的时候运行。而对于那些没有太多 I/O操作的代码而言,更倾向于在该线程整个时间片内始终占有处理器和GIL 。换句话说,I/O 密集型的Python程序要比计算密集型的代码更好的利用多线程环境。

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

退出线程

当一个线程完成函数的执行时,它就会退出。另外,还可以通过诸如thread.exit()之类的退出函数,或者 sys.exit()之类的退出Python 进程的标准方法,或者抛出SystenExit异常,来使线程退出。不过,你不能直接 "终止"一个线程。

下一节将会讨论两个与线程有关的Python模块,不过在这两个模块中,不建议使用thread模块。给出这个建议有很多原因,其中最明显的一个原因是在主线程退出后,所有其它线程都会在没有被清理前的情况下直接退出。而另一个模块threading会确保在所有“重要的”子线程退出前,保持整个进程的存活。

而主线程应该做一个好的管理者,负责了解每个单独的线程执行什么,每个派生的线程需要哪些数据或参数,这些线程执行完成后会提供什么样的后果。这样,主线程就可以收集每个线程的结果,然后汇总成一个有意义的结果。

在Python中使用线程

python虽然支持多线程编程,但还是需要取决于它所运行的操作系统。
默认情况下,从源码构建的Python(2.0及以上的版本)或者Win32二进制安装的Python,线程支持是已经启用的。要确定你的解释器是否支持线程,只需要从Shell窗口中尝试导入thread模块即可,如下所示(如果线程是可用的,则不会产生错误)。

    >>>import thread
    >>>

如果你的Python 解释器没有将线程支持编译进去,模块导入将会失败。

     >>>import thread
     Traceback (innermost last):
           File "", line 1, in?
     ImportError: No module named thread

这种情况下你需要重新编译你的Python解释器才能够使用线程。一般可以调用configure脚本的时候使用--with-thread选项。查阅你所使用的发行版本的README 文件,来获取如何在你的操作系统中编译线程支持的Python的指定命令。

不使用线程的情况

下面代码将使用 time.sleep() 函数来演示线程是如何工作的。`time.sleep()函数需要一个浮点型的参数,然后以这个给定的秒数进行“睡眠”,也就是说,程序的执行会暂时停止指定的时间。


from time import sleep,ctime

def loop0():
    print('start loop 0 at:', ctime())
    sleep(4)
    print('loop 0 done at:', ctime())

def loop1():
    print('start loop 1 at:', ctime())
    sleep(2)
    print('loop 1 done at:', ctime())

def main():
    print('starting at:',ctime())
    loop0()
    loop1()
    print('all Done at:', ctime())

if __name__ == '__main__':
    main()

创建两个时间循环:一个睡眠4秒(loop0());另一个睡眠2秒(loo1p())。如果在一个单进程或者单线程的程序中顺序执行loop0()loop1(),整个执行时间会达到6秒钟。而在启动loop0()loop1()以及执行其它代码时,也可能存在一秒钟的开销,使得整个时间达到7秒。

可以通过执行代码验证这一点,下面是输出结果。

starting at: Tue Jan 23 09:48:47 2018
start loop 0 at: Tue Jan 23 09:48:47 2018
loop 0 done at: Tue Jan 23 09:48:51 2018
start loop 1 at: Tue Jan 23 09:48:51 2018
loop 1 done at: Tue Jan 23 09:48:53 2018
all Done at: Tue Jan 23 09:48:53 2018

现在,假设loop0()loop1()中的操作不是睡眠,而是执行独立计算操作的函数,所有结果汇成一个最终结果。那么,让它们并行执行来减少总的执行时间是不是有用呢?这就是现在要介绍的多线程编程的前提。

Python的threading模块

Python提供了多个模块来支持多线程编程。包括,thread、threading、Queue模块等。程序是可以提供thread、threading模块来创建于管理线程。thread模块提供基本的线程和锁定支持;而threading模块提供更高级、功能更加全面的线程管理。使用Queue 模块,用户可以创建一个队列数据结构,用于在多线程之间进行共享。我们将分别查看这几个模块,并给出几个例子和中等规模的应用。

核心提示:避免使用thread模块
原因是threading模块更加先进,有更好的线程支持,并且thread模块中的一些属性和threading模块有冲突。另一个原因是低级别的thread模块拥有的同步原语很少(实际上只有一个),而threading模块则有很多。
不过处于对Python和线程学习的兴趣,我们将给出使用使用 thread 模块的一些代码,给出这些代码只是出于学习目的, 希望它能够更好的让你领悟为什么应该避免使用threading模块。我们还将展示如何使用更加合适的工具,如threading和Queue 模块中的那些方法。
避免使用thread模块的另一个原因是它对于进程何时退出没有控制,当主线程结束时,其他线程也都强制结束,不会发出警告或者进行适当的清理。如前所述,至少threading模块能确保重要的子线程在进程退出前结束。

你可能感兴趣的:(多线程编程(三) 线程和Python)