python浓缩(18)多线程编程

  • 引言/动机

  • 线程和进程

  • 线程和 Python

  • thread 模块

  • threading 模块

  • 生产者-消费者问题和 Queue 模块

  • 相关模块

本节将探索用多线程编程技术实现代码并行性的几种不同的方法。在前面几节中将介绍进程与线程的区别。然后介绍多线程编程的概念。最后演示如何使用threading 和Queue 模块来实现多线程编程。

18.1 引言/动机

多线程编程出现之前,程序的运行由一个执行序列组成,执行序列按顺序在主机的中央处理器(CPU)中运行。无论是任务本身要求顺序执行还是整个程序是由多个子任务组成,程序都是按这种方式执行的。即使子任务相互独立,互相无关时也是这样。是不是不合逻辑?会不会想要并行运行这些相互独立的子任务呢?这样的并行处理可以大幅度地提升整个任务的效率。这就是多线程编程的目的。

多线程编程对于某些任务来说,是最理想的。这些任务具有以下特点:它们本质上就是异步的,需要有多个并发事务,各个事务的运行顺序可以是不确定的,随机的,不可预测的。这样的编程任务可以被分成多个执行流,每个流都有一个要完成的目标。根据应用的不同,这些子任务可能都要计算出一个中间结果,用于合并得到最后的结果。

运算密集型的任务一般都比较容易分隔成多个子任务,可以顺序执行或以多线程的方式执行。

单线程处理多个外部输入源的任务就不是那么容易了。这种编程任务如果不用多线程的方式处理,则一定要使用一个或多个计时器来实现。

一个顺序执行的程序要从每个I/O(输入/输出)终端信道检查用户的输入时,程序无论如何也不能在读取I/O 终端信道的时候阻塞。因为用户输入的到达是不确定的,阻塞会导致其它I/O 信息的数据不能被处理。顺序执行的程序必须使用非阻塞I/O,或是带有计时器的阻塞I/O(这样才能保证阻塞只是暂时的)。

由于顺序执行的程序只有一个线程在运行。它要保证它要做的多任务,不会有某个任务占用太多的时间,而且要合理地分配用户的响应时间。执行多任务的顺序执行的程序一般程序控制流程都很复杂,难以理解。

使用多线程编程和一个共享的数据结构如Queue(本章后面会介绍的一种多线程队列数据结构),这种程序任务可以用几个功能单一的线程来组织:

  • UserRequestThread: 负责读取客户的输入,可能是一个I/O 信道。程序可能创建多个线程,每个客户一个,请求会被放入队列中。

  • RequestProcessor: 一个负责从队列中获取并处理请求的线程,它为下面那种线程提供输出。

  • ReplyThread: 负责把给用户的输出取出来,如果是网络应用程序就把结果发送出去,否则就保存到本地文件系统或数据库中。

把这种编程任务用多线程来组织可以降低程序的复杂度,并使得干净,有效和具有良好组织地程序结构实现变得可能。每个线程的逻辑都不会很复杂,每个线程都有自己明确的任务。

18.2 线程和进程

18.2.1 什么是进程?

计算机程序只不过是磁盘中可执行的,二进制(或其它类型)的数据。它们只有在被读取到内存中,被操作系统调用的时候才开始它们的生命期。进程(有时被称为重量级进程)是程序的一次执行。每个进程都有自己的地址空间,内存,数据栈以及其它记录其运行轨迹的辅助数据。操作系统管理在其上运行的所有进程,并为这些进程公平地分配时间。进程也可以通过fork 和spawn 操作来完成其它的任务。不过各个进程有自己的内存空间,数据栈等,所以只能使用进程间通讯(IPC),而不能直接共享信息。

18.2.2 什么是线程?

线程(有时被称为轻量级进程)跟进程有些相似,不同的是,所有的线程运行在同一个进程中,共享相同的运行环境。它们可以想像成是在主进程或“主线程”中并行运行的“迷你进程”。

线程有开始,顺序执行和结束三部分。它有一个自己的指令指针,记录自己运行到什么地方。线程的运行可能被抢占(中断),或暂时的被挂起(也叫睡眠),让其它的线程运行,这叫做让步。一个进程中的各个线程之间共享同一片数据空间,所以线程之间可以比进程之间更方便地共享数据以及相互通讯。线程一般都是并发执行的,正是由于这种并行和数据共享的机制使得多个任务的合作变为可能。实际上,在单CPU 的系统中,真正的并发是不可能的,每个线程会被安排成每次只运行一小会,然后就把CPU 让出来,让其它的线程去运行。在进程的整个运行过程中,每个线程都只做自己的事,在需要的时候跟其它的线程共享运行的结果。

当然,这样的共享并不是完全没有危险的。如果多个线程共同访问同一片数据,则由于数据访问的顺序不一样,有可能导致数据结果的不一致的问题。这叫做竞态条件(race condition)。幸运的是,大多数线程库都带有一系列的同步原语,来控制线程的执行和数据的访问。

另一个要注意的地方是,由于有的函数会在完成之前阻塞住,在没有特别为多线程做修改的情况下,这种“贪婪”的函数会让CPU 的时间分配有所倾斜。导致各个线程分配到的运行时间可能不尽相同,不尽公平。

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

18.3.1 全局解释器锁(GIL)

代码的执行由Python 虚拟机(也叫解释器主循环)来控制。Python 设计之初就kao虑到要在主循环中,同时只有一个线程在执行,就像单CPU 的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在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 的(会调用内建的操作系统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。

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

>>> import thread
>>>

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

18.3.4 没有线程支持的情况

第一个例子使用time.sleep()函数来演示线程是怎样工作的。这就意味着,程序的运行会被挂起指定的时间。创建两个“计时循环”。一个睡眠4 秒种,一个睡眠2 秒种,分别是loop0()和loop1()。(“loop0”和“loop1”表示将有一个循环的序列)。如下,在一个进程或一个线程中,顺序地执行loop0()和loop1(),那运行的总时间为6 秒。在启动loop0(),loop1(),和其它的代码时,也要花去一些时间,所以,总时间也有可能会是7 秒钟。

例18.1 单线程中运行的循环 (onethr.py)在单线程中顺序执行两个循环。一定要一个循环结束后,另一个才能开始。总时间是各个循环运行时间之和。

#!/usr/bin/env python

from time import sleep, ctime

def loop():
    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()
    loop();
    loop1();
    print 'all DONE at:', ctime()

if __name__ == "__main__":
    main()

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

$ onethr.py
starting at: Sun Aug 13 05:03:34 2006
start loop 0 at: Sun Aug 13 05:03:34 2006
loop 0 done at: Sun Aug 13 05:03:38 2006
start loop 1 at: Sun Aug 13 05:03:38 2006
loop 1 done at: Sun Aug 13 05:03:40 2006
all DONE at: Sun Aug 13 05:03:40 2006

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

18.3.5 Python 的threading 模块

Python 提供了几个用于多线程编程的模块,包括thread, threading 和Queue 等。thread 和threading 模块允许程序员创建和管理线程。thread 模块提供了基本的线程和锁的支持,而threading提供了更高级别,功能更强的线程管理的功能。Queue 模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。

核心提示:避免使用thread 模块

  • 首先,更高级别的threading 模块更为先进,对线程的支持更为完善,而且使用thread 模块里的属性有可能会与threading 出现冲突。

  • 其次,低级别的thread 模块的同步原语很少(实际上只有一个),而threading 模块则有很多。将给出一点使用thread 模块的例子,只用于学习目的,让你对为什么应该避免使用thread 模块有更深的认识,以及让你了解在把代码改为使用threading 和Queue 模块时,能获得多大的便利。

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

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

18.4 thread 模块

thread 模块除了产生线程外,thread 模块也提供了基本的同步数据结构锁对象(lock object,也叫原语锁,简单锁,互斥锁,互斥量,二值信号量)。如之前所说,同步原语与线程的管理是密不可分的。

表18.1 中所列的是常用的线程函数以及LockType 类型的锁对象的方法。start_new_thread()函数是thread 模块的一个关键函数,它的语法与内建的apply()函数完全一样,其参数为:函数,函数的参数以及可选的关键字参数。不同的是,函数不是在主线程里运行,而是产生一个新的线程来运行这个函数。现在,把线程加入到onethr.py 例子中。稍微改变一下loop*()函数的调用方法,得到例18.2 的mtsleep1.py。

表18.1 thread 模块和锁对象

python浓缩(18)多线程编程_第1张图片

例18.2 使用thread 模块 (mtsleep1.py)

这儿执行的是和onethr.py 中一样的循环,不同的是,这一次使用的是thread 模块提供的简单的多线程的机制。两个循环并发地被执行(显然,短的那个先结束)。总的运行时间为最慢的那个线程的运行时间,而不是所有的线程的运行时间之和。

#!/usr/bin/env python

import thread
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()
    thread.start_new_thread(loop0, ())
    thread.start_new_thread(loop1, ())
    sleep(6)
    print 'all done at:', ctime()


if __name__=='__main__':
    main()

start_new_thread()要求一定要有前两个参数。所以,就算我们想要运行的函数不要参数,我们也要传一个空的元组。这个程序的输出与之前的输出大不相同,之前是运行了6,7 秒,而现在则是4 秒,是最长的循环的运行时间与其它的代码的时间总和。

$ mtsleep1.py
starting at: Sun Aug 13 05:04:50 2006
start loop 0 at: Sun Aug 13 05:04:50 2006
start loop 1 at: Sun Aug 13 05:04:50 2006
loop 1 done at: Sun Aug 13 05:04:52 2006
loop 0 done at: Sun Aug 13 05:04:54 2006
all DONE at: Sun Aug 13 05:04:56 2006

loop1 甚至在loop0 前面就结束了。程序的不同之处就是多了一个“sleep(6)”的函数调用。如果没有让主线程停下来,那主线程就会运行下一条语句,显示“all done”,然后就关闭运行着loop0()和loop1()的两个线程,退出了。

我们没有写让主线程停下来等所有子线程结束之后再继续运行的代码。这就是我们之前说线程需要同步的原因。使用了sleep()函数做为我们的同步机制。应该有什么好的管理线程的方法,而不是在主线程里做一个额外的延时6 秒的操作。因为这样总的运行时间并不比单线程少。而且,使用sleep()函数做线程的同步操作是不可靠的。如果循环的执行时间不能事先确定的话,那怎么办呢?可能造成主线程过早或过晚退出。这就是锁的用武之地了。

再一次修改程序为mtsleep2.py,引入锁的概念。其输出与mtsleep1.py 很相似,唯一的区别是不用为线程什么时候结束再做额外的等待。使用了锁,就可以在两个线程都退出后,马上退出。例18.3 使用线程和锁 (mtsleep2.py) :

#!/usr/bin/env python

import thread
from time import sleep, ctime

loops=[4,2]

def loop(nloop, nsec, lock):
    print 'start loop', nloop, 'at:', ctime()
    sleep(nsec)
    print 'loop','nloop','done at:',ctime()
    lock.release

def main():
    print 'starting at:', ctime()
    locks = []
    nloops = range(len(loops))
    for i in nloops: 
        lock = thread.allocate_lock()
        lock.acquire()
        locks.append(lock)

    for i in nloops:
        thread.start_new_thread(loop, (i, loops[i], locks[i]))

    for i in nloops:
        while locks[i].locked(): 
            pass
    print 'all DONE at:', ctime()

if __name__=="__main__":
    main()

loop()函数里增加了一些锁的操作。一个明显的改变是,要在函数中记录下循环的号码和要睡眠的时间。最后一个不一样的地方就是那个锁了。每个线程都会被分配一个事先已经获得的锁,在sleep()的时间到了之后就释放相应的锁以通知主线程,这个线程已经结束了。

主要的工作在包含三个循环的main()函数中完成。

  • 先调用thread.allocate_lock()函数创建一个锁的列表,并分别调用各个锁的acquire()函数获得锁。获得锁表示“把锁锁上”,锁上后,就把锁放到锁列表locks 中。

  • 下一个循环创建线程,每个线程都用各自的循环号,睡眠时间和锁为参数去调用loop()函数。为什么不在创建锁的循环里创建线程呢?有以下几个原因:(1) 我们想到实现线程的同步,所以要让“所有的马同时冲出栅栏”。(2) 获取锁要花一些时间,如果你的线程退出得“太快”,可能会导致还没有获得锁,线程就已经结束了的情况。在线程结束的时候,线程要自己去做解锁操作。

  • 最后一个循环只是坐在那一直等(达到暂停主线程的目的),直到两个锁都被解锁为止才继续运行。由于顺序检查每一个锁,可能会要长时间地等待运行时间长且放在前面的线程,当这些线程的锁释放之后,后面的锁可能早就释放了(表示对应的线程已经运行完了)。结果主线程只能毫不停歇地完成对后面这些锁的检查。

使用thread 模块只是为了给读者演示如何进行多线程编程。你的多线程程序应该使用更高级别的模块,如threading 等。现在我们就开始讨论它。

18.5 threading 模块

更高级别的threading 模块不仅提供了Thread 类,还提供了各种非常好用的同步机制。表18.2 列出了threading 模块里所有的对象。这一节会演示如何使用Thread 类来实现多线程。之前已经介绍过锁的基本概念,这里我们将不会提到锁原语。而Thread 类也有某种同步机制,所以,没有必要详细介绍锁原语。

表18.2 threading 模块对象

python浓缩(18)多线程编程_第2张图片

核心提示:守护线程

另一个避免使用thread 模块的原因是,它不支持守护线程。当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出。有时,我们并不期望这种行为,这时,就引入了守护线程的概念。threading 模块支持守护线程,它们是这样工作的:守护线程一般是一个等待客户请求的服务器,如果没有客户提出请求,它就在那等着。如果你设定一个线程为守护线程,就表示你在说这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。就像你在第16 章网络编程看到的,服务器线程运行在一个无限循环中,一般不会退出。

如果你的主线程要退出的时候,不用等待那些子线程完成,那就设定这些线程的daemon 属性。即,在线程开始(调用thread.start())之前,调用setDaemon()函数设定线程的daemon 标志(thread.setDaemon(True))就表示这个线程“不重要”如果你想要等待子线程完成再退出, 那就什么都不用做, 或者显式地调用thread.setDaemon(False)以保证其daemon 标志为False。你可以调用thread.isDaemon()函数来判断其daemon 标志的值。新的子线程会继承其父线程的daemon 标志。整个Python 会在所有的非守护线程退出后才会结束,即进程中没有非守护线程存在的时候才结束。

18.5.1 Thread 类

threading 的Thread 类是你主要的运行对象。它有很多thread 模块里没有的函数,详见表18.3。

用Thread 类,可以用多种方法来创建线程。介绍三种比较相像的方法。选择最适合你的程序以及最能满足程序可扩展性的(我们一般比较喜欢最后一个选择):

  • 创建一个 Thread 的实例,传给它一个函数

  • 创建一个 Thread 的实例,传给它一个可调用的类对象

  • 从 Thread 派生出一个子类,创建一个这个子类的实例

表18.3 Thread 对象的函数

python浓缩(18)多线程编程_第3张图片

创建一个Thread 的实例,传给它一个函数

第一个例子将初始化一个Thread 对象,把函数(及其参数)像上一个例子那样传进去。在线程开始执行的时候,这个函数会被执行。把mtsleep2.py 脚本拿过来,做一些调整加入Thread对象的使用,就成了例18.4 中的mtsleep3.py。运行的输出跟之前很相似。都做了些什么修改呢?在使用thread 模块时使用的锁没有了。新加了一些Thread 对象。

在实例化每个Thread 对象的时候,把函数(target)和参数(args)传进去,得到返回的Thread实例。实例化一个Thread(调用Thread())与调用thread.start_new_thread()之间最大的区别就是,新的线程不会立即开始。在你创建线程对象,但不想马上开始运行线程的时候,这是一个很有用的同步特性。

例18.4 使用threading 模块 (mtsleep3.py)threading 模块的Thread 类有一个join()函数,允许主线程等待线程的结束。

from time import sleep, ctime
import threading

loops = [4,2]

def loop(nloop, sec):
    print 'loop begin', nloop, ' at ', ctime()
    sleep(sec)
    print 'loop end', nloop, ' at ', ctime()

def main():
    print 'main begin',ctime()
    threads = []
    nloops = range(len(loops))
    for i in nloops:
        t = threading.Thread(target = loop, args = (i, loops[i]))
        threads.append(t)
    for i in nloops:
        threads[i].start()

    for i in nloops:
        threads[i].join()

    print 'main end',ctime()


if __name__ == '__main__':
    main()

所有的线程都创建了之后再一起调用start()函数启动,而不是创建一个启动一个。而且,不用再管理一堆锁(分配锁,获得锁,释放锁,检查锁的状态等),只要简单地对每个线程调用join()函数就可以了。

join()会等到线程结束,或者在给了timeout 参数的时候,等到超时为止。使用join()看上去会比使用一个等待锁释放的无限循环清楚一些(这种锁也被称为"spinlock")。join()的另一个比较重要的方面是它可以完全不用调用。一旦线程启动后,就会一直运行,直到线程的函数结束,退出为止。如果你的主线程除了等线程结束外,还有其它的事情要做(如处理或等待其它的客户请求),那就不用调用join(),只有在你要等待线程结束的时候才要调用join()

创建一个Thread 的实例,传给它一个可调用的类对象

与传一个函数很相似的另一个方法是在创建线程的时候,传一个可调用的类的实例供线程启动的时候执行——这是多线程编程的一个更为面向对象的方法。相对于一个或几个函数来说,由于类对象里可以使用类的强大的功能,可以保存更多的信息,这种方法更为灵活。

把ThreadFunc 类加入到mtsleep3.py 代码中,就得到了mtsleep4.py。这次又改了些什么呢?主要是增加了ThreadFunc 类和创建Thread 对象时会实例化一个可调用类ThreadFunc 的类对象。也就是说,我们实例化了两个对象。

我们想让这个类在调用什么函数方面尽量地通用,并不局限于那个loop()函数。所以加了一些修改,如,这个类保存了函数的参数,函数本身以及函数的名字字符串。构造函数__init__()里做了这些值的赋值工作。创建新线程的时候,Thread 对象会调用我们的ThreadFunc 对象,这时会用到一个特殊函数__call__()。由于我们已经有了要用的参数,所以就不用再传到Thread()的构造函数中。由于我们有一个参数的元组,这时要在代码中使用apply()函数。如果你使用的是Python1.6 或是更高版本,你可以使用11.6.3 节中所说的新的调用语法,而不用像第16 行那样使用apply()函数:

self.res = self.func(*self.args)

例18.5 使用可调用的类 (mtsleep4.py) 

from time import sleep, ctime
import threading

loops = [4,2]
class ThreadFunc(object):
    def __init__(self, func, args, name=""):
        self.func = func
        self.args = args
        self.name = name

    def __call__(self):
        apply(self.func, self.args)

def loop(nloop, sec):
    print 'loop begin', nloop, ' at ', ctime()
    sleep(sec)
    print 'loop end', nloop, ' at ', ctime()

def main():
    print 'main begin',ctime()
    threads = []
    nloops = range(len(loops))
    for i in nloops:
        t = threading.Thread(target=ThreadFunc(loop, (i, loops[i]),
            loop.__name__))
        threads.append(t)
    for i in nloops:
        threads[i].start()

    for i in nloops:
        threads[i].join()

    print 'main end',ctime()


if __name__ == '__main__':
    main()

从Thread 派生出一个子类,创建一个这个子类的实例

最后一个例子介绍如何子类化Thread 类。比较mtsleep4 和mtsleep5 两个模块的代码之前,指出最重要的两点改变:

(1)MyThread 子类的构造函数一定要先调用基类的构造函数;

(2)之前的特殊函数__call__()在子类中,名字要改为run();

例18.6 子类化Thread (mtsleep5.py)我们现在要子类化Thread 类,而不是创建它的实例。这样做可以更灵活地定制我们的线程对象,而且在创建线程的时候也更简单。

from time import sleep, ctime
import threading

loops = [4,2]
class MyThread(threading.Thread):
    def __init__(self, func, args, name=""):
        threading.Thread.__init__(self)
        self.func = func
        self.args = args
        self.name = name

    def run(self):
        apply(self.func, self.args)

def loop(nloop, sec):
    print 'loop begin', nloop, ' at ', ctime()
    sleep(sec)
    print 'loop end', nloop, ' at ', ctime()

def main():
    print 'main begin',ctime()
    threads = []
    nloops = range(len(loops))
    for i in nloops:
        t = MyThread(loop, (i, loops[i]))
        threads.append(t)
    for i in nloops:
        threads[i].start()

    for i in nloops:
        threads[i].join()

    print 'main end',ctime()


if __name__ == '__main__':
    main()

在MyThread 类中,加入一些用于调试的输出信息,把代码保存到myThread 模块中(见例18.7),并在下面的例子中,导入这个类。除了简单地使用apply()函数来运行这些函数之外,我们还把结果保存到实现的self.res 属性中,并创建一个新的函数getResult()来得到结果。

18.5.4 斐波那契,阶乘和累加和

例18.8 中的mtfacfib.py 脚本比较了递归求斐波那契,阶乘和累加和函数的运行。脚本先在单线程中运行这三个函数,然后在多线程中做同样的事,以说明多线程的好处。

例18.7 MyThread 子类化Thread (myThread.py)为了让Thread 的子类更为通用,我们把子类单独放在一个模块中,并加上一个getResult()函数用以返回函数的运行结果。

import threading
from time import ctime

class MyThread(threading.Thread):
    def __init__(self, func, args, name=""):
        threading.Thread.__init__(self)
        self.func = func
        self.args = args
        self.name = name

    def run(self):
        print 'begin ', self.name, 'at ', ctime()
        self.res = apply(self.func, self.args)
        print 'end ', self.name, 'at ', ctime()

    def getResult(self):
        return self.res

在单线程中运行只要逐个调用函数,在函数结束后,显示对应的结果。在多线程中,我们不马上显示结果。由于我们想让MyThread 类尽可能地通用(能同时适应有输出和没输出的函数),我们会等到要结束时才会调用getResult()函数,并在最后显示每个函数的结果。

由于这些函数运行得很快(斐波那契函数会慢一些),在每个函数中加上一个sleep()函数,让函数慢下来,以便于我们能方便地看到多线程能在多大程度上加速程序的运行。例18.8 斐波那契,阶乘和累加和 (mtfacfib.py)在这个多线程程序中,运行三个递归函数。

import threading
from time import sleep
from MyThread import MyThread

def fib(x):
    sleep(0.005)
    if x < 2: 
        return 1
    return (fib(x-2) + fib(x-1))

def fac(x):
    sleep(0.1)
    if x < 2: 
        return 1
    return (x * fac(x-1))

def sum(x):
    sleep(0.1)
    if x < 2: 
        return 1
    return (x + sum(x-1))

funcs = [fib, fac, sum]
n = 12


def main():
    nfunc = range(len(funcs))
    threads = []
    for i in nfunc:
        t = MyThread(funcs[i], (n,))
        threads.append(t)

    for i in nfunc:
        threads[i].start()

    for i in nfunc:
        threads[i].join()
        print threads[i].getResult()

if __name__ == "__main__":
    main()

18.5.5 threading 模块中的其它函数

除了各种同步对象和线程对象外,threading 模块还提供了一些函数。如下 threading 模块的函数

python浓缩(18)多线程编程_第4张图片

18.5.5 生产者-消费者问题和Queue 模块

最后一个例子演示了生产者和消费者的场景。生产者生产货物,然后把货物放到一个队列之类的数据结构中,生产货物所要花费的时间无法预先确定。消费者消耗生产者生产的货物的时间也是不确定的。

表18.5 常用的Queue 模块的属性

python浓缩(18)多线程编程_第5张图片

Queue 模块可以用来进行线程间通讯,让各个线程之间共享数据。创建一个队列,让生产者(线程)把新生产的货物放进去供消费者(线程)使用。要使用到Queue模块的以下属性(见表18.5)。写出例18.9 的prodcons.py 的代码。输出:

$ prodcons.py
starting writer at: Sun Jun 18 20:27:07 2006
producing object for Q... size now 1
starting reader at: Sun Jun 18 20:27:07 2006
consumed object from Q... size now 0
producing object for Q... size now 1
consumed object from Q... size now 0
producing object for Q... size now 1
producing object for Q... size now 2
producing object for Q... size now 3
consumed object from Q... size now 2
consumed object from Q... size now 1
writer finished at: Sun Jun 18 20:27:17 2006
consumed object from Q... size now 0
reader finished at: Sun Jun 18 20:27:25 2006
all DONE

你会注意到,writer 睡眠的时间一般会比reader 睡眠的时间短。这可以减少reader 尝试从空队列中取数据的机会。writer 的睡眠时间短,那reader 在想要数据的时候总是能拿到数据。

例18.9 生产者-消费者问题 (prodcons.py) 这个实现中使用了Queue 对象和随机地生产(和消耗)货物的方式。生产者和消费者相互独立并且并发地运行。

from time import sleep
from random import randint
from MyThread import MyThread
from Queue import Queue

def writeQ(queue):
    print 'producing object for Q...',
    queue.put('xxx', 1)
    print "size now", queue.qsize()

def readQ(queue):
    val = queue.get(1)
    print 'consumed object from Q... size now', \
            queue.qsize()

def writer(queue, loops):
    for i in range(loops):
        writeQ(queue)
        sleep(randint(1, 3))

def reader(queue, loops):
    for i in range(loops):
        readQ(queue)
        sleep(randint(2, 5))

funcs = [writer, reader]
nfuncs = range(len(funcs))

def main():
    nloops = randint(2, 5)
    q = Queue(32)

    threads = []
    for i in nfuncs:
        t = MyThread(funcs[i], (q, nloops), funcs[i].__name__)
        threads.append(t)

    for i in nfuncs:
        threads[i].start()

    for i in nfuncs:
        threads[i].join()

    print 'ALL DONE'

if __name__ == "__main__":
    main()

本例中了解到,一个要完成多项任务的程序,可以kao虑每个任务使用一个线程。这样的程序在设计上相对于单线程做所有事的程序来说,更为清晰明了。

本章中,我们看到了单线程的程序在程序性能上的限制。尤其在有相互独立的,运行时间不确定的多个任务的程序里,把多个任务分隔成多个线程同时运行会比顺序运行速度更快。由于Python解释器是单线程的,所以不是所有的程序都能从多线程中得到好处。不过,你已经对Python 下的多线程有所了解,在适当的时候,可以利用它来改善程序的性能。


你可能感兴趣的:(python浓缩(18)多线程编程)