9.3.4 Python 多线程编程

很多编程语言都支持多线程编程,Python 语言亦然。与其他编程语言相比,Python 的 多线程编程是非常简单的。

Python 提供了两个支持线程的模块,一个是较老的 thread 模块,另一个是较新的 threading 模块。其中 threading 采用了面向对象实现,功能更强,建议读者使用。

thread 模块的用法

任何程序一旦开始执行,就构成了一个主线程。在主线程中随时可以创建新的子线程去

执行特定任务,并且主线程和子线程是并发执行的。 每个线程的生命期包括创建、启动、执行和结束四个阶段。 当主线程结束退出时,它所创建的子线程是否继续生存(如果还没有结束的话)依赖于系统平台,有的平台直接结束各子线程,有的平台则允许子线程继续执行。

thread 模块提供了一个函数 start_new_thread 用于创建和启动新线程,其用法如下: start_new_thread(<函数>,<参数>) 本函数的具体功能是:创建一个新线程并立即返回,返回值是所创建的新线程的标识号(如 果需要可以赋值给一个变量)。新线程随之启动,所执行的任务就是<函数>的代码,它应该 在程序中定义。调用<函数>时传递的实参由<参数>指定,<参数>是一个元组,如果<函数> 没有形参,则<参数>为空元组。当<函数>执行完毕返回时,线程即告结束。

下面用一个例子来说明 thread 模块的用法。程序 9.3 采用孙悟空拔毫毛变出小猴子的故事来演示主线程与它所创建的子线程的关系。主线程是孙悟空,子线程是小猴子。

【程序 9.3】eg9_3.py

  1. # -*- coding: cp936 -*- import thread
  2. def task(tName,n):
  3. for i in range(n):
  4. print "%s:%d\n" % (tName,i)
  5. print "%s:任务完成!回真身去喽...\n" % tName
  6. thread.interrupt_main()
  7. print "<老孙>:我是孙悟空!"
  8. print "<老孙>:我拔根毫毛变个小猴儿~~~"
  9. thread.start_new_thread(task,("<小猴>",3))
  10. print "<老孙>:我睡会儿,小猴儿干完活再叫醒我~~~\n"
  11. while True:
  12. print "<老孙>:Zzzzzzz\n"

程序执行后,孙悟空(主线程)先说了两句话,然后创建小猴子,最后进入一个无穷循 环。小猴子创建后就立即启动,执行函数 task,该函数的任务只是显示简单的信息。task 函 数的最后一行调用 thread 模块中定义的 interrupt_main 函数,该函数的功能是在主线程中引 发 KeyboardInterrupt 异常,从而中断主线程的执行。如果没有这条语句,主线程将一直处于 无穷循环之中而无法结束。下面是程序的执行效果:

  1. <老孙>:我是孙悟空!
  2. <老孙>:我拔根毫毛变个小猴儿~~~
  3. <老孙>:我睡会儿,小猴儿干完活再叫醒我~~~
  4. <小猴>:0
  5. <老孙>:Zzzzzzz
  6. <小猴>:1
  7. <老孙>:Zzzzzzz
  8. <小猴>:2
  9. <老孙>:Zzzzzzz
  10. <小猴>:任务完成!回真身去喽...
  11. <老孙>:Zzzzzzz
  12. Traceback (most recent call last): File "eg9_3.py", line 15, in <module>
  13. print "<老孙>:Zzzzzzz\n"
  14. KeyboardInterrupt

从输出结果可见,主线程和子线程确实是在以交叉方式并行执行。 顺便说一下,由于程序 9.3 中使用了汉字,所以要在程序的第一行使用

  1. # -*- coding: cp936 -*-

其作用是告诉 Python 解释器代码中使用了 cp936 编码的字符(即汉字)。 下面再看一个例子。程序 9.4 的主线程创建了两个子线程,两个子线程都执行同一个函数 task,但以不同的节奏来显示信息(每次循环中利用 sleep 分别休眠 2 秒和 4 秒)。注意, 与程序 9.3 不同,主线程创建完子线程后就结束了,留下两个子线程继续执行①。

【程序 9.4】eg9_4.py

  1. # -*- coding: cp936 -*- import thread
  2. import time
  3. def task(tName,n,delay):
  4. for i in range(n):
  5. time.sleep(delay)
  6. print "%s:%d\n" % (tName,i)
  7. print "<老孙>:我是孙悟空!"
  8. print "<老孙>:我拔根毫毛变个小猴儿<哼>"
  9. thread.start_new_thread(task,("<哼>",5,2))
  10. print "<老孙>:我再拔根毫毛变个小猴儿<哈>"
  11. thread.start_new_thread(task,("<哈>",5,4))
  12. print "<老孙>:不管你们喽,俺老孙去也~~~\n"

下面是程序 9.4 的一次执行结果:

  1. <老孙>:我是孙悟空!
  2. <老孙>:我拔根毫毛变个小猴儿<哼>
  3. <老孙>:我再拔根毫毛变个小猴儿<哈>
  4. <老孙>:不管你们喽,俺老孙去也~~~
  5. >>> <哼>:0
  6. <哈>:0
  7. <哼>:1
  8. <哼>:2
  9. <哈>:1
  10. <哼>:3
  11. <哼>:4
  12. ![](img/程序设计思想与方法293617.png)① 在作者所用的计算机平台(Windows XP + Python 2.7)上,主线程结束并不会导致子线程结束。
  13. <哈>:2
  14. <哈>:3
  15. <哈>:4
  16. KeyboardInterrupt
  17. >>>

注意在“<哼>:0”之前的 Python 解释器提示符“>>>”,它表明主线程已经结束,控制 已返回给 Python 解释器。但后续的输出表明两个子线程还在继续执行。读者可以分析一下 输出结果所显示的<哼>、<哈>交叉执行的情况,并想想为什么是这样的结果。另外要注意, 由于主线程先于两个子线程结束,导致两个子线程执行结束后无法返回,这时可以用组合键 Ctrl-C 来强行中止子线程,屏幕上出现的 KeyboardInterrupt 就是因此而来。

threading 模块的用法

虽然 thread 模块用起来很方便,但它的功能有限,不如较新的 threading 模块。threading 模块采用面向对象方式来实现对线程的支持,其中定义了类 Thread,这个类封装了有关线 程创建、启动等功能。

Thread 类的使用有两种方式。第一种用法是:直接利用 Thread 类来创建线程对象,并 在创建时向 Thread 构造器传递线程将要执行的函数;创建后通过调用线程对象的 start()方法 来启动线程,以执行指定的任务。这是使用 threading 模块来创建线程的最简单方式。具体 语法如下:

  1. t = Thread(target=&lt;函数&gt;,args=&lt;参数&gt;)
  2. t.start()

可见,创建线程对象时,需向 Thread 类的构造器传递两个参数:线程所执行的<函数>以及 该函数所需的<参数>。注意这里采用了关键字参数的传递方式。

下面的程序 9.5 与程序 9.4 的几乎是一样的,只是采用了 Thread 类来创建线程对象。

【程序 9.5】eg9_5.py

  1. # -*- coding: cp936 -*-
  2. from threading import Thread from time import sleep
  3. def task(tName,n,delay):
  4. for i in range(n):
  5. sleep(delay)
  6. print "%s:%d\n" % (tName,i)
  7. print "<老孙>:我是孙悟空!"
  8. print "<老孙>:我拔根毫毛变个小猴儿<哼>"
  9. t1 = Thread(target=task,args=("<哼>",5,2))
  10. print "<老孙>:我再拔根毫毛变个小猴儿<哈>"
  11. t2 = Thread(target=task,args=("<哈>",5,4))
  12. t1.start()
  13. t2.start()
  14. print "<老孙>:不管你们喽,俺老孙去也~~~\n"

另一种使用 Thread 类的方法用到了线程对象的这么一个特性:当用 start()方法启动线程 对象时,系统会自动调用 run()方法。因此,只要我们将线程需要执行的任务代码写到 run() 方法中,就能在创建并启动线程对象后自动执行该任务。而将自己的代码写到 run()方法中 可以通过定义子类并重定义 run()的方式来做到。

总之,我们可以通过下列步骤来创建并启动线程,执行指定的任务。

(1)定义 Thread 类的子类。这相当于定制我们自己的线程对象。

(2)重定义 init()方法,即定制我们自己的构造器来初始化线程对象,例如可以添 加更多的参数。注意,定制构造器时首先应当执行基类的构造器 Thread.init (),因为我 们定制的线程是原始线程的特例,首先要符合原始线程的要求。

(3)重定义 run()方法,即指定我们定制的线程将执行的代码。

(4)利用自定义的线程类创建线程实例,并通过调用该实例的 start()方法(间接调用 run()方法)或直接调用 run()方法来启动新线程执行任务。

程序 9.6 是采用上述方法的一个例子。

  1. # -*- coding: cp936 -*-
  2. from threading import Thread
  3. from time import sleep
  4. exitFlag = False
  5. class myThread(Thread):
  6. def __init__ (self,tName,n,delay):
  7. Thread. __init__ (self)
  8. self.name = tName
  9. self.loopnum = n
  10. self.delay = delay
  11. def run(self):
  12. print self.name + ": 上场...\n" task(self.name,self.loopnum,self.delay)
  13. print self.name + ": 退场...\n"
  14. def task(tName,n,delay): for i in range(n):
  15. if exitFlag:
  16. print "<哼>已退场,<哈>也提前结束吧~~~\n" return
  17. sleep(delay)
  18. print "%s:%d\n" % (tName,i)
  19. print "<老孙>:我是孙悟空!"
  20. print "<老孙>:我拔根毫毛变个小猴儿<哼>"
  21. t1 = myThread("<哼>",5,2)
  22. t1.start()
  23. print "<老孙>:我再拔根毫毛变个小猴儿<哈>\n"
  24. t2 = myThread("<哈>",10,4)
  25. t2.start()
  26. while t2.isAlive():
  27. if not t1.isAlive():
  28. exitFlag = True
  29. print "<老孙>:小猴儿们都回了,俺老孙去也~~~"

当线程启动后,就处于“活着(alive)”的状态,直到线程的 run()方法执行结束(不管 是正常结束还是因为发生异常而终止),该线程才结束“活着”状态。线程对象的 is_alive() 方法可用来检查线程是否活着。程序 9.6 中,主线程在创建并启动两个子线程 t1 和 t2 之后, 就一直检测 t2 是否还活着,如果 t2 活着就接着检测 t1 是否活着。当 t2 活着而 t1 已经结束, 则将一个用作退出标志的全局变量 exitFlag 设置为 True(初始值为 False)。而 t2 执行任务循 环时会检测 exitFlag 变量,一旦发现它变成了 True,就知道另一个线程已经结束,于是不再 执行后面的任务,直接结束返回。读者应该注意到这件事的意义,它意味着多个线程之间是 可以进行协作、同步的,而不是各自只管闷着头做自己的事情。

以下是程序 9.6 的一次执行结果:

  1. <老孙>:我是孙悟空!
  2. <老孙>:我拔根毫毛变个小猴儿<哼>
  3. <哼>: 上场...
  4. <老孙>:我再拔根毫毛变个小猴儿<哈>
  5. <哈>: 上场...
  6. <哼>:0
  7. <哼>:1
  8. <哈>:0
  9. <哼>:2
  10. <哈>:1
  11. <哼>:3
  12. <哼>:4
  13. <哼>: 退场...
  14. <哈>:2
  15. <哼>已退场,<哈>也提前结束吧~~~
  16. <哈>: 退场...
  17. <老孙>:小猴儿都回了,俺老孙去也~~~
  18. >>>

线程有名字,可通过 getName()和 setName()方法读出或设置线程名。 总是有一个主线程对象,它对应于程序的初始控制流。

并发计算中的同步问题

多个线程之间如果只是彼此独立地执行各自的任务,事情就简单了。但是实际应用中常常需要多个线程之间进行合作,合作的线程往往要存取一些公共数据。由于多个线程的执行 顺序是不可预测的,这就有可能导致公共数据处于不一致的状态。因此,如果允许多个线程 并发读写公共数据,就必须对多线程的交互进行控制,以保护公共数据的正确性。

程序 9.6 演示了两个线程通过一个全局变量进行协同的例子。

又如,Thread 对象具有一个 join()方法,一个线程对象 t1 可以调用另一个线程对象 t2 的 join()方法,这导致 t1 暂停执行,直至 t2 执行结束(或者执行一个指定的时间)。可见, join 方法能实现让一个线程等待另一个线程以便进行某种同步的目的。

threading 模块还实现了更一般的同步机制,在此就不介绍了。