SECTION 29 多线程编程

多线程编程

    • 29.1 引入动机
    • 29.2 线程和进程
        • 29.2.1 什么是进程
        • 29.2.2 什么是线程
    • 29.3 Python、线程和全局解释器锁
        • 29.3.1 全局解释器锁(GIL)
        • 29.2.2 退出线程
        • 29.2.3 在 Python 中使用线程
        • 29.2.4 没有线程支持的情况
        • 29.2.5 Python 的 threading 模块
    • 29.3 thread 模块
    • 29.4 threading 模块
        • 29.4.1 threading 模块对象
        • 29.4.2核心提示:守护线程
        • 29.4.3 Thread 类
          • 29.4.3.1 创建一个 Thread 的实例,传给它一个函数
          • 29.4.3.2 创建 Thread 的实例,传给它一个可调用的类实例
          • 29.4.3.3 派生 Thread 的子类,并创建子类的实例
        • 29.4.4 threading 模块的其他函数
    • 29.5 单线程和多线程执行对比
    • 29.6 多线程实践
        • 29.6.1 图书排名示例
        • 29.6.2 引入线程

29.1 引入动机

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

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

运算密集型的任务一般都比较容易分隔成多个子任务,可以顺序执行或以多线程的方式执行。 单线程处理多个外部输入源的的任务就不是那么容易了。这种编程任务如果不用多线程的方式处理, 则一定要使用一个或多个计时器来实现。

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

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

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

UserRequestThread: 负责读取客户的输入,可能是一个 I/O 信道。程序可能创建多个线程, 每个客户一个,请求会被放入队列中。
RequestProcessor: 一个负责从队列中获取并处理请求的线程,它为下面那种线程提供输出。
ReplyThread: 负责把给用户的输出取出来,如果是网络应用程序就把结果发送出去,否则 就保存到本地文件系统或数据库中。

把这种编程任务用多线程来组织可以降低程序的复杂度,并使得干净,有效和具有良好组织地 程序结构实现变得可能。每个线程的逻辑都不会很复杂,因为它要做的事情很清楚。例如, UserRequestThread 只是从用户或某个数据源读取数据,放到一个队列中,等待其它线程进一步的 处理,等等,每个线程都有自己明确的任务。你只要设计好每个线程要做什么,并把要做的事做好 就可以了。对某些任务使用线程跟亨利福特制造汽车时使用的装配线模型有些相似。

29.2 线程和进程

29.2.1 什么是进程

计算机程序只不过是磁盘中可执行的,二进制(或其它类型)的数据。它们只有在被读取到内 存中,被操作系统调用的时候才开始它们的生命期。进程(有时被称为重量级进程)是程序的一次执行。每个进程都有自己的地址空间,内存,数据栈以及其它记录其运行轨迹的辅助数据。

操作系统管理在其上运行的所有进程,并为这些进程公平地分配时间。进程也可以通过 fork 和 spawn 操作 来完成其它的任务。不过各个进程有自己的内存空间,数据栈等,所以只能使用进程间通讯(IPC), 而不能直接共享信息。

29.2.2 什么是线程

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

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

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

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

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

29.3.1 全局解释器锁(GIL)

Python 代码的执行由 **Python 虚拟机(也叫解释器主循环)**来控制。Python 在设计之初就考虑到 要在主循环中,同时只有一个线程在执行,就像单 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 程序比计算密集型的程序更能充分利用多线程环境的好处。

29.2.2 退出线程

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

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

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

29.2.3 在 Python 中使用线程

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

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

>>> import thread 
>>>> 

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

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

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

29.2.4 没有线程支持的情况

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

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

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

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

if __name__ == '__main__':
	main()
start at: Tue Dec 27 15:11:59 2022
start loop 0 at: Tue Dec 27 15:11:59 2022
loop 0 done at: Tue Dec 27 15:12:03 2022
start loop 1 at: Tue Dec 27 15:12:03 2022
loop 1 done at: Tue Dec 27 15:12:05 2022
all DONE at: Tue Dec 27 15:12:05 2022

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

29.2.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 等高级别的线程模块。

29.3 thread 模块

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

下表中所列的是常用的线程函数以及 LockType 类型的锁对象的方法。

start_new_thread()函数是 thread 模块的一个关键函数,它的语法与内建的 apply()函数完全 一样,其参数为:函数,函数的参数以及可选的关键字参数。不同的是,函数不是在主线程里运行, 而是产生一个新的线程来运行这个函数。

现在,把线程加入到我们的 onethr.py 例子中。稍微改变一下 loop*()函数的调用方法,我们得到了下例的mtsleep1.py。

from time import sleep, ctime
import thread

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 'start at:', ctime()
	thread.start_new_thread(loop,())
	thread.start_new_thread(loop1,())
	sleep(6)
	print 'all DONE at:', ctime()

if __name__ == '__main__':
	main()
start at: Tue Dec 27 15:23:52 2022
start loop 0 at: Tue Dec 27 15:23:52 2022
start loop 1 at: Tue Dec 27 15:23:52 2022
loop 1 done at: Tue Dec 27 15:23:54 2022
loop 0 done at: Tue Dec 27 15:23:56 2022
all DONE at: Tue Dec 27 15:23:58 2022

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

睡眠 4 秒和 2 秒的代码现在是并发执行的。这样,就使得总的运行时间被缩短了。你可以看到, loop1 甚至在 loop0 前面就结束了。程序的一大不同之处就是多了一个“sleep(6)”的函数调用。为 什么要加上这一句呢?因为,如果我们没有让主线程停下来,那主线程就会运行下一条语句,显示 “all done”,然后就关闭运行着 loop0()和 loop1()的两个线程,退出了。

你也许在想,应该有什么好的管理线程的方法,而不是在主线程里做一个额外的延时 6 秒的操 作。因为这样一来,我们的总的运行时间并不比单线程的版本来得少。而且,像这样使用 sleep() 函数做线程的同步操作是不可靠的。如果我们的循环的执行时间不能事先确定的话,那怎么办呢? 这可能造成主线程过早或过晚退出。这就是锁的用武之地了。

from time import sleep, ctime
import thread

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 'start at:', ctime()
	locks=[]
	nloops=range(len(loops))

	for _ 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()
start at: Tue Dec 27 15:42:20 2022
start loop 0 at:start loop  Tue Dec 27 15:42:20 20221
 at: Tue Dec 27 15:42:20 2022
loop 1 done at: Tue Dec 27 15:42:22 2022
loop 0 done at: Tue Dec 27 15:42:24 2022
all DONE at: Tue Dec 27 15:42:24 2022

在 main()函数中,我们先调用 thread.allocate_lock()函数创 建一个锁的列表,并分别调用各个锁的 acquire()函数获得锁。获得锁表示“把锁锁上”。锁上后, 我们就把锁放到锁列表 locks 中。下一个循环创建线程,每个线程都用各自的循环号,睡眠时间和 锁为参数去调用 loop()函数。为什么我们不在创建锁的循环里创建线程呢?有以下几个原因:(1) 我 们想到实现线程的同步,所以要让“所有的马同时冲出栅栏”。(2) 获取锁要花一些时间,如果你的 线程退出得“太快”,可能会导致还没有获得锁,线程就已经结束了的情况。

在线程结束的时候,线程要自己去做解锁操作。后一个循环只是坐在那一直等(达到暂停主 线程的目的),直到两个锁都被解锁为止才继续运行。由于我们顺序检查每一个锁,所以我们可能会 要长时间地等待运行时间长且放在前面的线程,当这些线程的锁释放之后,后面的锁可能早就释放 了(表示对应的线程已经运行完了)。结果主线程只能毫不停歇地完成对后面这些锁的检查。后两 行代码的意思你应该已经知道了,就是只有在我们直接运行这个脚本时,才运行 main()函数

29.4 threading 模块

接下来,我们要介绍的是更高级别的 threading 模块,它不仅提供了 Thread 类,还提供了各 种非常好用的同步机制。

29.4.1 threading 模块对象

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

29.4.2核心提示:守护线程

另一个避免使用 thread 模块的原因是,它不支持守护线程。
当主线程退出时,所有的子线程不 论它们是否还在工作,都会被强行退出。有时,我们并不期望这种行为,这时,就引入了守护线程的概念 threading 模块支持守护线程,它们是这样工作的:守护线程一般是一个等待客户请求的服务器, 如果没有客户提出请求,它就在那等着。

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

29.4.3 Thread 类

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

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

你会发现你将选择第一个或第三个方案。当你需要一个更加符合面向对象的接口时, 会选择后者;否则会选择前者。老实说,你会发现第二种方案显得有些尴尬并且稍微难以 阅读

Thread 对象的函数

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

在第一个例子中,我们只是把 Thread 类实例化,然后将函数(及其参数)传递进去,和 之前例子中采用的方式一样。当线程开始执行时,这个函数也会开始执行。

from time import sleep, ctime
import threading

loops = [4,2]

def loop(nloop, nsec):
	print 'start loop',nloop,'at:', ctime()
	sleep(nsec)
	print 'loop',nloop,'done at:', ctime()
	
def main():
	print 'start 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:
		threads[i].start()

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

	print 'all DONE at:',ctime()

if __name__ == '__main__':
	main()
$ mtsleepC.py 
starting at: Sun Aug 13 18:16:38 2006 
start loop 0 at: Sun Aug 13 18:16:38 2006 
start loop 1 at: Sun Aug 13 18:16:38 2006 
loop 1 done at: Sun Aug 13 18:16:40 2006 
loop 0 done at: Sun Aug 13 18:16:42 2006 
all DONE at: Sun Aug 13 18:16:42 2006 

那么,这里到底做了哪些修改呢?使用 thread 模块时实现的锁没有了,取而代之的是一 组 Thread 对象。当实例化每个 Thread 对象时,把函数(target)和参数(args)传进去,然 后得到返回的 Thread 实例。实例化 Thread(调用 Thread())和调用 thread.start_new_thread() 的最大区别是新线程不会立即开始执行。这是一个非常有用的同步功能,尤其是当你并不希 望线程立即开始执行时。 当所有线程都分配完成之后,通过调用每个线程的 start()方法让它们开始执行,而不是 在这之前就会执行。相比于管理一组锁(分配、获取、释放、检查锁状态等)而言,这里只 需要为每个线程调用 join()方法即可。join()方法将等待线程结束,或者在提供了超时时间的 情况下,达到超时时间。使用 join()方法要比等待锁释放的无限循环更加清晰(这也是这种锁 又称为自旋锁的原因)。

对于 join()方法而言,其另一个重要方面是其实它根本不需要调用。一旦线程启动,它们 就会一直执行,直到给定的函数完成后退出。如果主线程还有其他事情要去做,而不是等待 这些线程完成(例如其他处理或者等待新的客户端请求),就可以不调用 join()。join()方法只 有在你需要等待线程完成的时候才是有用的。

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

在创建线程时,与传入函数相似的一个方法是传入一个可调用的类的实例,用于线程执 行——这种方法更加接近面向对象的多线程编程。这种可调用的类包含一个执行环境,比起 一个函数或者从一组函数中选择而言,有更好的灵活性。现在你有了一个类对象,而不仅仅 是单个函数或者一个函数列表/元组。

from time import sleep, ctime
import threading

loops = [4,2]

class ThreadFunc(object):

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

	def __call__(self):
		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 'start at:', 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 'all DONE at:',ctime()

if __name__ == '__main__':
	main()
$ mtsleepD.py 
start at: Tue Dec 27 17:13:28 2022
start loop 0 at: Tue Dec 27 17:13:28 2022
start loop 1 at: Tue Dec 27 17:13:28 2022
loop 1 done at: Tue Dec 27 17:13:30 2022
loop 0 done at: Tue Dec 27 17:13:32 2022
all DONE at: Tue Dec 27 17:13:32 2022

那么,这次又修改了什么呢?主要是添加了 ThreadFunc 类,并在实例化 Thread 对象时做了一点小改动,同时实例化了可调用类 ThreadFunc。实际上,这里完成了两个实例化。

让我们先仔细看看 ThreadFunc 类吧。 我们希望这个类更加通用,而不是局限于 loop()函数,因此添加了一些新的东西,比如让这个类保存了函数的参数、函数自身以及函数名的字符串。而构造函数__init__()用于设定上述这些值。 当创建新线程时,Thread 类的代码将调用 ThreadFunc 对象,此时会调用__call__()这个特殊方法。由于我们已经有了要用到的参数,这里就不需要再将其传递给 Thread()的构造函 数了,直接调用即可。

29.4.3.3 派生 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.name = name
		self.func = func
		self.args = args

	def run(self):
		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 'start at:', ctime()
	threads=[]
	nloops=range(len(loops))

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

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

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

	print 'all DONE at:',ctime()

if __name__ == '__main__':
	main()
start at: Tue Dec 27 17:19:48 2022
start loop 0 at: Tue Dec 27 17:19:48 2022
start loop 1 at: Tue Dec 27 17:19:48 2022
loop 1 done at: Tue Dec 27 17:19:50 2022
loop 0 done at: Tue Dec 27 17:19:52 2022
all DONE at: Tue Dec 27 17:19:52 2022

当比较前两个模块的代码时,注意其中的几个重要变化: 1)MyThread 子类的构造函数必须先调用其基类的构造函数; 2)之前的特殊方法__call__()在这个 子类中必须要写为 run()。

现在,对 MyThread 类进行修改,增加一些调试信息的输出,并将其存储为一个名为myThread 的独立模块,以便在接下来的例子中导入这个类。除了简单地调用函 数外,还将把结果保存在实例属性 self.res 中,并创建一个新的方法 getResult()来获取这个值。

from time import sleep, ctime
import threading

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):
		print 'starting',self.name,'at:', ctime()
		self.res = self.func(*self.args)
		print self.name,'finished at:', ctime()

29.4.4 threading 模块的其他函数

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

函 数 描 述
activeCount/ active_count()① 当前活动的 Thread 对象个数
current Thread() /current_thread① 返回当前的 Thread 对象
enumerate() 返回当前活动的 Thread 对象列表
settrace (func) ② 为所有线程设置一个 trace 函数
setprofile (func) ② 为所有线程设置一个 profile 函数
stack_size (size=0) ③ 返回新创建线程的栈大小;或为后续创建的线程设定栈的大小 的size

① 驼峰式命名已经弃用,并且从 Python 2.6 版本起已经开始被取代。 ② 自 Python 2.3 版本开始引入。 ③ thread.stack_size()的一个别名,(都是)从 Python 2.5 版本开始引入的。

29.5 单线程和多线程执行对比

from time import sleep, ctime
import threading

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):
		print 'starting',self.name,'at:', ctime()
		self.res = self.func(*self.args)
		print self.name,'finished at:', ctime()

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 'strating', 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()
*** SINGLE THREAD
strating fib at: Tue Dec 27 17:37:07 2022
233
fib finished at: Tue Dec 27 17:37:09 2022
strating fac at: Tue Dec 27 17:37:09 2022
479001600
fac finished at: Tue Dec 27 17:37:11 2022
strating sum at: Tue Dec 27 17:37:11 2022
78
sum finished at: Tue Dec 27 17:37:12 2022

*** MULTIPLE THREADS
starting fib at: Tue Dec 27 17:37:12 2022
starting fac at: Tue Dec 27 17:37:12 2022
starting sum at: Tue Dec 27 17:37:12 2022
fac finished at: Tue Dec 27 17:37:13 2022
sum finished at: Tue Dec 27 17:37:13 2022
fib finished at: Tue Dec 27 17:37:15 2022
233
479001600
78
ALL DONE

以单线程模式运行时,只是简单地依次调用每个函数,并在函数执行结束后立即显示相 应的结果。 而以多线程模式运行时,并不会立即显示结果。因为我们希望让 MyThread 类越通用越好(有输出和没有输出的调用都能够执行),我们要一直等到所有线程都执行结束,然后调用 getResult()方法来最终显示每个函数的返回值。 因为这些函数执行起来都非常快(也许斐波那契函数除外),所以你会发现在每个函数中 都加入了 sleep()调用,用于减慢执行速度,以便让我们看到多线程是如何改善性能的。在实 际工作中,如果确实有不同的执行时间,你肯定不会在其中调用 sleep()函数。

29.6 多线程实践

到目前为止,我们已经见到的这些简单的示例片段都无法代表你要在实践中写出的代码。 除了演示多线程和创建线程的不同方式外,之前的代码实际上什么有用的事情都没有做。我 们启动这些线程以及等待它们结束的方式都是一样的,它们也全都睡眠。

由于 Python 虚拟机是单线程(GIL)的原因,只有线程在执行 I/O 密集 型的应用时才能更好地发挥 Python 的并发性(对比计算密集型应用,它只需要做轮询),因此让我们看一个 I/O 密集型的例子

29.6.1 图书排名示例

下例的 bookrank.py脚本非常直接。它将前往我最喜欢的在线零售商之一 Amazon, 然后请求你希望查询的图书的当前排名。在这个示例代码中,你可以看到函数 getRanking() 使用正则表达式来拉取和返回当前的排名,而函数_showRanking()用于向用户显示结果。 请记住,根据 Amazon 的使用条件,“ Amazon 对您在本网站的访问和个人使用授予有限 许可,未经 Amazon 明确的书面同意,不允许对全部或部分内容进行下载(页面缓存除外) 或修改。”在该程序中,我们所做的只是查询指定书籍的当前排名,没有任何其他操作,甚至 都不会对页面进行缓存。 下例是我们对于 bookrank.py的第一次(不过与最终版本也很接近了)尝试,这是一个没有使用线程的版本。

from atexit import register
from re import compile
from threading import Thread
from time import ctime
from urllib2 import urlopen as uopen

REGEX = compile('#([\d,]+) in Books ')
AMZN = 'http://amazon.com/dp/'
ISBNs = {
	'0132269937':	'Core Python Programming',
	'0132356139':	'Python Web Development with Django',
	'0137143419':	'Python Fundamentals',
}

def getRanking(isbn):
	page = uopen('%s%s' % (AMZN, isbn))
	data = page.read()
	page.close()
	return REGEX.findall(data)[0]

def _showRanking(isbn):
	print '- %r ranked %s' % (ISBNs[isbn], getRanking(isbn))

def main():
	print 'At', ctime(), 'on Amazon...'
	for isbn in ISBNs:
		_showRanking(isbn)

@register
def _atexit():
	print 'all DONE at:', ctime()

if __name__ == '__main__':
	main()

由于网络问题,笔者无法得到正常运行结果,贴上书上的结果:

$ python bookrank.py
 At Wed Mar 30 22:11:19 2011 PDT on Amazon... 
 - 'Core Python Programming' ranked 87,118 
 - 'Python Fundamentals' ranked 851,816 
 - 'Python Web Development with Django' ranked 184,735 
 all DONE at: Wed Mar 30 22:11:25 2011

29.6.2 引入线程

不需要你告诉我这仍然是一个愚蠢的单线程程序,我们接下来就要使用多线程来修改这 个应用。由于这是一个 I/O 密集型应用,因此这个程序使用多线程是一个好的选择。简单起 见,我们不会在这里使用任何类和面向对象编程,而是使用 threading 模块。我们将直接使用Thread 类,所以你可以认为这更像是 mtsleepC.py 的衍生品,而不是它之后的例子。我们将只是派生线程,然后立即启动这些线程。

将应用中的_showRanking(isbn)进行如下修改。
Thread(target=_showRanking, args=(isbn,)).start()

$ python bookrank.py 
At Thu Mar 31 10:11:32 2011 on Amazon... 
- 'Python Fundamentals' ranked 869,010 
- 'Core Python Programming' ranked 36,481
- 'Python Web Development with Django' ranked 219,228 
all DONE at: Thu Mar 31 10:11:35 2011

你可能感兴趣的:(Python基础,数据库,java,网络)