Python基础知识-多线程编程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shenwanjiang111/article/details/78802350

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

1.1 全局解释器锁(GIL)

Python 代码的执行由 Python 虚拟机(也叫解释器主循环)来控制。Python 在设计之初就考虑到要在主循环中,同时只有一个线程在执行,就像单 CPU 的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在 CPU 中运行。同样地,虽然 Python 解释器中可以“运行”多个线程,但在任意时刻,只有一个线程在解释器中运行。
对 Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。在多线程环境中,Python 虚拟机按以下方式执行:

  • 设置 GIL
  • 切换到一个线程去运行
  • 运行:
    a. 指定数量的字节码指令,或者
    b. 线程主动让出控制(可以调用 time.sleep(0))
  • 把线程设置为睡眠状态
  • 解锁 GIL
  • 再次重复以上所有步骤

在调用外部代码(如 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 文件。

1.2 退出线程

线程可以调用 thread.exit()之类的退出函数,也可以使用Python 退出进程的标准方法,如 sys.exit()或抛出一个 SystemExit 异常等。不过,你不可以直接“杀掉”(“kill”)一个线程。
主线程应该是一个好的管理者,它要了解每个线程都要做些什么事,线程都需要什么数据和什么参数,以及在线程结束的时候,它们都提供了什么结果。这样,主线程就可以把各个线程的结果组合成一个有意义的最后结果。

1.3 在 Python 中使用线程

想要从解释器里判断线程是否可用,只要简单的在交互式解释器里尝试导入 thread 模块就行了,只要没出现错误就表示线程可用。

import thread

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

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

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

1.4 没有线程支持的情况

在下面的代码中,我们创建两个函数分别是 loop0()和 loop1(),一个睡眠 4 秒,一个睡眠 2 秒。我们可以看一下在单线程中运行时总的运行时间为6s或者更多一点(依据系统不同)。
例 1 单线程中运行的循环 (one_thread.py)

#!/usr/bin/env python

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()

运行结果:

$ python one_thread.py 
starting at: Tue Dec 12 10:23:33 2017
start loop 0 at: Tue Dec 12 10:23:33 2017
loop 0 done at: Tue Dec 12 10:23:37 2017
start loop 1 at: Tue Dec 12 10:23:37 2017
loop 1 done at: Tue Dec 12 10:23:39 2017
all DONE at: Tue Dec 12 10:23:39 2017

1.5 Python 的 threading 模块

Python 提供了几个用于多线程编程的模块,包括 thread, threading 和 Queue 等。thread 和threading 模块允许程序员创建和管理线程。 thread 模块提供了基本的线程和锁的支持, 而 threading提供了更高级别,功能更强的线程管理的功能。Queue 模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。我们将分别介绍这几个模块,并给出一些例子和中等大小的应用。
**核心提示:避免使用 thread 模块
出于以下几点考虑,我们不建议您使用 thread 模块。 首先,更高级别的 threading 模块更为先进, 对线程的支持更为完善, 而且使用 thread 模块里的属性有可能会与 threading 出现冲突。 其次,低级别的 thread 模块的同步原语很少(实际上只有一个),而 threading 模块则有很多。
不过,出于对学习 Python 和线程的兴趣,我们将给出一点使用 thread 模块的例子。这些代码只用于学习目的,让你对为什么应该避免使用 thread 模块有更深的认识,以及让你了解在把代码改为使用 threading 和 Queue 模块时,我们能获得多大的便利。
另一个不要使用 thread 原因是,对于你的进程什么时候应该结束完全没有控制,当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作。我们之前说过,至少threading 模块能确保重要的子线程退出后进程才退出。
只建议那些有经验的专家在想访问线程的底层结构的时候,才使用 thread 模块。而使用线程的新手们则应该看看我们是如何把线程应用到我们的第一个程序,从而增加代码的可读性,以及第一段例子如何进化到我们本章的主要的代码的。如果可以的话,你的第一个多线程程序应该尽可能地使用 threading 等高级别的线程模块。**
核心提示:避免使用 thread 模块
出于以下几点考虑,我们不建议您使用 thread 模块。 首先,更高级别的 threading 模块更为先进, 对线程的支持更为完善, 而且使用 thread 模块里的属性有可能会与 threading 出现冲突。 其次,低级别的 thread 模块的同步原语很少(实际上只有一个),而 threading 模块则有很多。
另一个不要使用 thread 原因是,对于你的进程什么时候应该结束完全没有控制,当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作。我们之前说过,至少threading 模块能确保重要的子线程退出后进程才退出。
只建议那些有经验的专家在想访问线程的底层结构的时候,才使用 thread 模块。而使用线程的新手们则应该看看我们是如何把线程应用到我们的第一个程序,从而增加代码的可读性,以及第一段例子如何进化到我们本章的主要的代码的。如果可以的话,你的第一个多线程程序应该尽可能地使用 threading 等高级别的线程模块

2 threading 模块

表 2 threading 模块对象

  • Thread 表示一个线程的执行的对象
  • Lock 锁原语对象(跟 thread 模块里的锁对象相同)
  • RLock 可重入锁对象。使单线程可以再次获得已经获得了的锁(递归锁定)。
  • Condition 条件变量对象能让一个线程停下来, 等待其它线程满足了某个“条件”。如,状态的改变或值的改变。
  • Event 通用的条件变量。多个线程可以等待某个事件的发生,在事件发生后,所有的线程都会被激活。
  • Semaphore 为等待锁的线程提供一个类似“等候室”的结构
  • BoundedSemaphore 与 Semaphore 类似,只是它不允许超过初始值
  • Timer 与 Thread 相似,只是,它要等待一段时间后才开始运行。

核心提示:守护线程
另一个避免使用 thread 模块的原因是,它不支持守护线程。当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出。有时,我们并不期望这种行为,这时,就引入了守护线程的概念。
threading 模块支持守护线程, 它们是这样工作的: 守护线程一般是一个等待客户请求的服务器,如果没有客户提出请求,它就在那等着。如果你设定一个线程为守护线程,就表示你在说这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。 就像你在第 16 章网络编程看到的,服务器线程运行在一个无限循环中,一般不会退出。
如果你的主线程要退出的时候,不用等待那些子线程完成,那就设定这些线程的 daemon 属性。即,在线程开始(调用 thread.start())之前, 调用 setDaemon()函数设定线程的 daemon 标志(thread.setDaemon(True)) 就表示这个线程“不重要。”
如果你想要等待子线程完成再退出,那就什么都不用做,或者显式地调用thread.setDaemon(False)以保证其 daemon 标志为 False。 你可以调用 thread.isDaemon()函数来判断其 daemon 标志的值。新的子线程会继承其父线程的 daemon 标志。整个 Python 会在所有的非守护线程退出后才会结束,即进程中没有非守护线程存在的时候才结束。

2.1 Thread 类

threading 的 Thread 类是你主要的运行对象。 它有很多 thread 模块里没有的函数, 详见表 18.3。用 Thread 类,你可以用多种方法来创建线程。我们在这里介绍三种比较相像的方法。你可以任选一种你喜欢的,或最适合你的程序以及最能满足程序可扩展性的(我们一般比较喜欢最后一个选择):

  • (1)创建一个 Thread 的实例,传给它一个函数
  • (2)创建一个 Thread 的实例,传给它一个可调用的类对象
  • (3)从 Thread 派生出一个子类,创建一个这个子类的实例

表 3 Thread 对象的函数

  • start() 开始线程的执行
  • run() 定义线程的功能的函数(一般会被子类重写)
  • join(timeout=None) 程序挂起,直到线程结束;如果给了
  • timeout,则最多阻塞 timeout 秒
  • getName() 返回线程的名字
  • setName(name) 设置线程的名字
  • isAlive() 布尔标志,表示这个线程是否还在运行中
  • isDaemon() 返回线程的 daemon 标志
  • setDaemon(daemonic) 把线程的 daemon 标志设为
  • daemonic(一定要在调用 start()函数前调用) 创建一个 Thread 的实例,传给它一个函数

第一个例子的中,我们将初始化一个 Thread 对象,把函数(及其参数)像上一个例子那样传进去。 在线程开始执行的时候, 这个函数会被执行。 把 mtsleep2.py 脚本拿过来, 做一些调整加入 Thread对象的使用,就成了例 2 中的 mtsleep3.py。
运行的输出跟之前很相似:

$ python mtsleep3.py 
starting at: Tue Dec 12 11:28:40 2017
start loop 0 at: Tue Dec 12 11:28:40 2017
start loop 1 at: Tue Dec 12 11:28:40 2017
loop 1 done at: Tue Dec 12 11:28:42 2017
loop 0 done at: Tue Dec 12 11:28:44 2017
all DONE at: Tue Dec 12 11:28:44 2017

那么, 都做了些什么修改呢?在使用 thread 模块时使用的锁没有了。 新加了一些 Thread 对象。在实例化每个 Thread 对象的时候, 我们把函数(target) 和参数(args) 传进去, 得到返回的 Thread实例。实例化一个 Thread(调用 Thread())与调用 thread.start_new_thread()之间最大的区别就是,新的线程不会立即开始。在你创建线程对象,但不想马上开始运行线程的时候,这是一个很有用的同步特性。
例 2 使用 threading 模块 (mtsleep3.py)
threading 模块的 Thread 类有一个 join()函数,允许主线程等待线程的结束。

#!/usr/bin/env python

import threading
from time import sleep, ctime

loops = [4,2]

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

def main():
    print 'starting at:', 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: # start threads
        threads[i].start()

    for i in nloops:       # wait for all
        threads[i].join()  # threads to finish

    print 'all DONE at:', ctime()    

if __name__ == '__main__':
    main()

所有的线程都创建了之后,再一起调用 start()函数启动,而不是创建一个启动一个。而且,不用再管理一堆锁(分配锁, 获得锁, 释放锁, 检查锁的状态等), 只要简单地对每个线程调用 join()函数就可以了。
join()会等到线程结束,或者在给了 timeout 参数的时候,等到超时为止。使用 join()看上去会比使用一个等待锁释放的无限循环清楚一些(这种锁也被称为”spinlock”)。
join()的另一个比较重要的方面是它可以完全不用调用。一旦线程启动后,就会一直运行,直到线程的函数结束,退出为止。如果你的主线程除了等线程结束外,还有其它的事情要做(如处理或等待其它的客户请求), 那就不用调用 join(), 只有在你要等待线程结束的时候才要调用 join()。
创建一个 Thread 的实例,传给它一个可调用的类对象
与传一个函数很相似的另一个方法是在创建线程的时候,传一个可调用的类的实例供线程启动的时候执行——这是多线程编程的一个更为面向对象的方法。相对于一个或几个函数来说,由于类对象里可以使用类的强大的功能,可以保存更多的信息,这种方法更为灵活。
把 ThreadFunc 类加入到 mtsleep3.py 代码中,并做一些其它的小修改后,就得到了例 3 中的 mtsleep4.py。运行它,就会得到如下的输出:

$ python mtsleep4.py 
starting at: Tue Dec 12 11:52:17 2017
start loop 0 at: Tue Dec 12 11:52:17 2017
start loop 1 at: Tue Dec 12 11:52:17 2017
loop 1 done at: Tue Dec 12 11:52:19 2017
loop 0 done at: Tue Dec 12 11:52:21 2017
all DONE at: Tue Dec 12 11:52:21 2017

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

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

例 3 使用可调用的类 (mtsleep4.py)
此例中,我们传了一个可调用的类(的实例),而不是仅传一个函数。相对 mtsleep3.py 中的方法来说,这样做更具面向对象的概念。

#!/usr/bin/env python

import threading
from time import sleep, ctime

loops = [4,2]

class ThreadFunc(object):

    def __init__(self, func, args, name=''):
        self.name = name
        self.func = func
        self.args = args

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

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

def main():
    print 'starting at:', ctime()
    threads = []
    nloops = range(len(loops))

    for i in nloops:
        #print loop.__name__
        t = threading.Thread(target=ThreadFunc(loop,(i, loops[i]), loop.__name__))
        threads.append(t)

    for i in nloops: # start threads
        threads[i].start()

    for i in nloops:       # wait for all
        threads[i].join()  # threads to finish

    print 'all DONE at:', ctime()    

if __name__ == '__main__':
    main()

从 Thread 派生出一个子类,创建一个这个子类的实例
最后一个例子介绍如何子类化 Thread 类,这与上一个例子中的创建一个可调用的类非常像。使用子类化创建线程(第 29-30 行)使代码看上去更清晰明了。我们将在例 4 中给出 mtsleep5.py的代码,以及代码运行的输出。比较 mtsleep5.py 和 mtsleep4.py 的任务则留给读者做为练习。
下面是 mtsleep5.py 的输出,同样,跟我们的期望一致:

$ python mtsleep5.py 
starting at: Tue Dec 12 13:46:40 2017
start loop 0 at: Tue Dec 12 13:46:40 2017
start loop 1 at: Tue Dec 12 13:46:40 2017
loop 1 done at: Tue Dec 12 13:46:42 2017
loop 0 done at: Tue Dec 12 13:46:44 2017
all DONE at: Tue Dec 12 13:46:44 2017

在读者比较 mtsleep4 和 mtsleep5 两个模块的代码之前,我们想指出最重要的两点改变:(1)我们的 MyThread 子类的构造函数一定要先调用基类的构造函数(第 9 行),(2)之前的特殊函数call()在子类中,名字要改为 run()。
现在,在 MyThread 类中,加入一些用于调试的输出信息,把代码保存到 myThread 模块中(见例18.7),并在下面的例子中,导入这个类。除了简单地使用 apply()函数来运行这些函数之外,我们还把结果保存到实现的 self.res 属性中,并创建一个新的函数 getResult()来得到结果。

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

例 18.8 中的 mtfacfib.py 脚本比较了递归求斐波那契,阶乘和累加和函数的运行。脚本先在单线程中运行这三个函数,然后在多线程中做同样的事,以说明多线程的好处。
例 18.6 子类化 Thread (mtsleep5.py)
我们现在要子类化 Thread 类, 而不是创建它的实例。 这样做可以更灵活地定制我们的线程对象,而且在创建线程的时候也更简单。

#!/usr/bin/env python

import threading
from time import sleep, ctime

loops = [4,2]

class MyThread(threading.Thread):

    def __init__(self, func, args, name=''):
        threading.Thread.__init__(self)
        self.name = name
        self.func = func
        self.args = args

    def run(self): # When a ThreadFunc object is called by a Thread object, two methods are all accepted. 
        #apply(self.func, self.args)
        self.res = self.func(*self.args)

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

def main():
    print 'starting at:', ctime()
    threads = []
    nloops = range(len(loops))

    for i in nloops:
        #print loop.__name__
        t = MyThread(loop, (i, loops[i]), loop.__name__)
        threads.append(t)

    for i in nloops: # start threads
        threads[i].start()

    for i in nloops:       # wait for all
        threads[i].join()  # threads to finish

    print 'all DONE at:', ctime()    

if __name__ == '__main__':
    main()

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

#!/usr/bin/env python

import threading
from time import sleep, ctime

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

    def getResult(self):
        return self.res

    def run(self):
        #apply(self.func, self.args)
        print 'starting', self.name, 'at:', ctime()
        self.res = self.func(*self.args)
        print self.name, 'finished at:', ctime()

在单线程中运行只要简单地逐个调用这些函数,在函数结束后,显示对应的结果。在多线程中,我们不马上显示结果。 由于我们想让 MyThread 类尽可能地通用(能同时适应有输出和没输出的函数),我们会等到要结束时才会调用 getResult()函数,并在最后显示每个函数的结果。
由于这些函数运行得很快(斐波那契函数会慢一些),你会看到,我们得在每个函数中加上一个 sleep()函数,让函数慢下来,以便于我们能方便地看到多线程能在多大程度上加速程序的运行。不过实际工作中,你一般不会想在程序中加上sleep()函数的。下面是程序的输出:

$ python mtfacfib.py 
*** SINGLE THREAD
starting fib at: Tue Dec 12 14:08:21 2017
233
fib finished at: Tue Dec 12 14:08:23 2017
starting fac at: Tue Dec 12 14:08:23 2017
479001600
fac finished at: Tue Dec 12 14:08:25 2017
starting sum at: Tue Dec 12 14:08:25 2017
78
sum finished at: Tue Dec 12 14:08:26 2017

*** MULTIPLE THREADS
starting fib at: Tue Dec 12 14:08:26 2017
starting fac at: Tue Dec 12 14:08:26 2017
starting sum at: Tue Dec 12 14:08:26 2017
fac finished at: Tue Dec 12 14:08:27 2017
sum finished at: Tue Dec 12 14:08:27 2017
fib finished at: Tue Dec 12 14:08:28 2017
233
479001600
78
all DONE

例 18.8 斐波那契,阶乘和累加和 (mtfacfib.py)
在这个多线程程序中,我们会分别在单线程和多线程环境中,运行三个递归函数。

#!/usr/bin/env python

from mythread import MyThread
from time import ctime, sleep

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():
    nfuncs = range(len(funcs))
    print '*** SINGLE THREAD'
    for i in nfuncs:
        print 'starting', funcs[i].__name__, 'at:', ctime()
        print funcs[i](n)
        print funcs[i].__name__, 'finished at:', ctime()

    print '\n*** MULTIPLE THREADS'
    threads = []
    for i in nfuncs:
        t = MyThread(funcs[i], (n,), funcs[i].__name__)
        threads.append(t)

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

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

    print 'all DONE'

if __name__ == '__main__':
    main()

2.5 threading 模块中的其它函数

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

  • activeCount() 当前活动的线程对象的数量
  • currentThread() 返回当前线程对象
  • enumerate() 返回当前活动线程的列表
  • settrace(func) 为所有线程设置一个跟踪函数
  • setprofile(func) 为所有线程设置一个profile 函数

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

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

  • Queue 模块函数
  • queue(size) 创建一个大小为 size 的 Queue 对象
  • Queue 对象函数
  • qsize() 返回队列的大小(由于在返回的时候,队列可能会被其它线程修改,所以这个值是近似值)
  • empty() 如果队列为空返回 True,否则返回 False
  • full() 如果队列已满返回 True,否则返回 False
  • put(item,block=0) 把 item 放到队列中,如果给了 block(不为
    0),函数会一直阻塞到队列中有空间为止get(block=0) 从队列中取一个对象,如果给了 block(不为0),函数会一直阻塞到队列中有对象为止。
  • Queue 模块可以用来进行线程间通讯,让各个线程之间共享数据。

现在,我们创建一个队列,让生产者(线程)把新生产的货物放进去供消费者(线程)使用。要达到这个目的,我们要使用到 Queue模块的以下属性(见表 18.5)。
很容易地,我们就能写出例 18.9 的 prodcons.py 的代码。

例 18.9 生产者-消费者问题 (prodcons.py)

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

#!/usr/bin/env python

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

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(3, 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()

逐行解释
1-6 行
在这个模块中,我们要使用Queue.Queue对象以及我们在例18.7中给出的线程类mythread.MyThread。我们将使用 random.randint()函数来随机的进行生产和消耗。并从 time 模块中导入了常用的属性。
8-16行
writeQ()和 readQ()函数分别用来把对象放入队列和消耗队列中的一个对象。在这里我们使用字符串’xxx’来表示队列中的对象。
18-26 行
writer()函数只做一件事,就是一次往队列中放入一个对象,等待一会,然后再做同样的事,一共做指定的次数,这个次数是由脚本运行时随机生成的。reader()函数做的事比较类似,只是它是用来消耗对象的。
你会注意到,writer 睡眠的时间一般会比 reader 睡眠的时间短。这可以减少 reader 尝试从空队列中取数据的机会。writer 的睡眠时间短,那 reader 在想要数据的时候总是能拿到数据。
28-29 行
设置有多少个线程要被运行。
31-47 行
最后,就到了 main()函数,它与之前的所有脚本的 main()函数都很像。先是创建所有的线程,然后运行它们,最后,等两个线程都结束后,得到最后的运行结果。
从本例中,我们可以了解到,一个要完成多项任务的程序,可以考虑每个任务使用一个线程。这样的程序在设计上相对于单线程做所有事的程序来说,更为清晰明了。
本章中,我们看到了单线程的程序在程序性能上的限制。尤其在有相互独立的,运行时间不确定的多个任务的程序里,把多个任务分隔成多个线程同时运行会比顺序运行速度更快。由于 Python解释器是单线程的,所以不是所有的程序都能从多线程中得到好处。不过,你已经对 Python 下的多线程有所了解,在适当的时候,可以利用它来改善程序的性能。

3 相关模块

下表列出了一些多线程编程中可能用得到的模块:
表 18.6 多线程相关的标准库模块

猜你喜欢

转载自blog.csdn.net/shenwanjiang111/article/details/78802350