### 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
```
# -*- coding: cp936 -*- import thread
def task(tName,n):
for i in range(n):
print "%s:%d\n" % (tName,i)
print "%s:任务完成!回真身去喽...\n" % tName
thread.interrupt_main()
print "<老孙>:我是孙悟空!"
print "<老孙>:我拔根毫毛变个小猴儿~~~"
thread.start_new_thread(task,("<小猴>",3))
print "<老孙>:我睡会儿,小猴儿干完活再叫醒我~~~\n"
while True:
print "<老孙>:Zzzzzzz\n"
```
程序执行后,孙悟空(主线程)先说了两句话,然后创建小猴子,最后进入一个无穷循 环。小猴子创建后就立即启动,执行函数 task,该函数的任务只是显示简单的信息。task 函 数的最后一行调用 thread 模块中定义的 interrupt_main 函数,该函数的功能是在主线程中引 发 KeyboardInterrupt 异常,从而中断主线程的执行。如果没有这条语句,主线程将一直处于 无穷循环之中而无法结束。下面是程序的执行效果:
```
<老孙>:我是孙悟空!
<老孙>:我拔根毫毛变个小猴儿~~~
<老孙>:我睡会儿,小猴儿干完活再叫醒我~~~
<小猴>:0
<老孙>:Zzzzzzz
<小猴>:1
<老孙>:Zzzzzzz
<小猴>:2
<老孙>:Zzzzzzz
<小猴>:任务完成!回真身去喽...
<老孙>:Zzzzzzz
Traceback (most recent call last): File "eg9_3.py", line 15, in <module>
print "<老孙>:Zzzzzzz\n"
KeyboardInterrupt
```
从输出结果可见,主线程和子线程确实是在以交叉方式并行执行。 顺便说一下,由于程序 9.3 中使用了汉字,所以要在程序的第一行使用
```
# -*- coding: cp936 -*-
```
其作用是告诉 Python 解释器代码中使用了 cp936 编码的字符(即汉字)。 下面再看一个例子。程序 9.4 的主线程创建了两个子线程,两个子线程都执行同一个函数 task,但以不同的节奏来显示信息(每次循环中利用 sleep 分别休眠 2 秒和 4 秒)。注意, 与程序 9.3 不同,主线程创建完子线程后就结束了,留下两个子线程继续执行①。
【程序 9.4】eg9_4.py
```
# -*- coding: cp936 -*- import thread
import time
def task(tName,n,delay):
for i in range(n):
time.sleep(delay)
print "%s:%d\n" % (tName,i)
print "<老孙>:我是孙悟空!"
print "<老孙>:我拔根毫毛变个小猴儿<哼>"
thread.start_new_thread(task,("<哼>",5,2))
print "<老孙>:我再拔根毫毛变个小猴儿<哈>"
thread.start_new_thread(task,("<哈>",5,4))
print "<老孙>:不管你们喽,俺老孙去也~~~\n"
```
下面是程序 9.4 的一次执行结果:
```
<老孙>:我是孙悟空!
<老孙>:我拔根毫毛变个小猴儿<哼>
<老孙>:我再拔根毫毛变个小猴儿<哈>
<老孙>:不管你们喽,俺老孙去也~~~
>>> <哼>:0
<哈>:0
<哼>:1
<哼>:2
<哈>:1
<哼>:3
<哼>:4
![](img/程序设计思想与方法293617.png)① 在作者所用的计算机平台(Windows XP + Python 2.7)上,主线程结束并不会导致子线程结束。
<哈>:2
<哈>:3
<哈>:4
KeyboardInterrupt
>>>
```
注意在“<哼>:0”之前的 Python 解释器提示符“>>>”,它表明主线程已经结束,控制 已返回给 Python 解释器。但后续的输出表明两个子线程还在继续执行。读者可以分析一下 输出结果所显示的<哼>、<哈>交叉执行的情况,并想想为什么是这样的结果。另外要注意, 由于主线程先于两个子线程结束,导致两个子线程执行结束后无法返回,这时可以用组合键 Ctrl-C 来强行中止子线程,屏幕上出现的 KeyboardInterrupt 就是因此而来。
threading 模块的用法
虽然 thread 模块用起来很方便,但它的功能有限,不如较新的 threading 模块。threading 模块采用面向对象方式来实现对线程的支持,其中定义了类 Thread,这个类封装了有关线 程创建、启动等功能。
Thread 类的使用有两种方式。第一种用法是:直接利用 Thread 类来创建线程对象,并 在创建时向 Thread 构造器传递线程将要执行的函数;创建后通过调用线程对象的 start()方法 来启动线程,以执行指定的任务。这是使用 threading 模块来创建线程的最简单方式。具体 语法如下:
```
t = Thread(target=<函数>,args=<参数>)
t.start()
```
可见,创建线程对象时,需向 Thread 类的构造器传递两个参数:线程所执行的<函数>以及 该函数所需的<参数>。注意这里采用了关键字参数的传递方式。
下面的程序 9.5 与程序 9.4 的几乎是一样的,只是采用了 Thread 类来创建线程对象。
【程序 9.5】eg9_5.py
```
# -*- coding: cp936 -*-
from threading import Thread from time import sleep
def task(tName,n,delay):
for i in range(n):
sleep(delay)
print "%s:%d\n" % (tName,i)
print "<老孙>:我是孙悟空!"
print "<老孙>:我拔根毫毛变个小猴儿<哼>"
t1 = Thread(target=task,args=("<哼>",5,2))
print "<老孙>:我再拔根毫毛变个小猴儿<哈>"
t2 = Thread(target=task,args=("<哈>",5,4))
t1.start()
t2.start()
print "<老孙>:不管你们喽,俺老孙去也~~~\n"
```
另一种使用 Thread 类的方法用到了线程对象的这么一个特性:当用 start()方法启动线程 对象时,系统会自动调用 run()方法。因此,只要我们将线程需要执行的任务代码写到 run() 方法中,就能在创建并启动线程对象后自动执行该任务。而将自己的代码写到 run()方法中 可以通过定义子类并重定义 run()的方式来做到。
总之,我们可以通过下列步骤来创建并启动线程,执行指定的任务。
(1)定义 Thread 类的子类。这相当于定制我们自己的线程对象。
(2)重定义 init__()方法,即定制我们自己的构造器来初始化线程对象,例如可以添 加更多的参数。注意,定制构造器时首先应当执行基类的构造器 Thread.__init (),因为我 们定制的线程是原始线程的特例,首先要符合原始线程的要求。
(3)重定义 run()方法,即指定我们定制的线程将执行的代码。
(4)利用自定义的线程类创建线程实例,并通过调用该实例的 start()方法(间接调用 run()方法)或直接调用 run()方法来启动新线程执行任务。
程序 9.6 是采用上述方法的一个例子。
```
# -*- coding: cp936 -*-
from threading import Thread
from time import sleep
exitFlag = False
class myThread(Thread):
def __init__ (self,tName,n,delay):
Thread. __init__ (self)
self.name = tName
self.loopnum = n
self.delay = delay
def run(self):
print self.name + ": 上场...\n" task(self.name,self.loopnum,self.delay)
print self.name + ": 退场...\n"
def task(tName,n,delay): for i in range(n):
if exitFlag:
print "<哼>已退场,<哈>也提前结束吧~~~\n" return
sleep(delay)
print "%s:%d\n" % (tName,i)
print "<老孙>:我是孙悟空!"
print "<老孙>:我拔根毫毛变个小猴儿<哼>"
t1 = myThread("<哼>",5,2)
t1.start()
print "<老孙>:我再拔根毫毛变个小猴儿<哈>\n"
t2 = myThread("<哈>",10,4)
t2.start()
while t2.isAlive():
if not t1.isAlive():
exitFlag = True
print "<老孙>:小猴儿们都回了,俺老孙去也~~~"
```
当线程启动后,就处于“活着(alive)”的状态,直到线程的 run()方法执行结束(不管 是正常结束还是因为发生异常而终止),该线程才结束“活着”状态。线程对象的 is_alive() 方法可用来检查线程是否活着。程序 9.6 中,主线程在创建并启动两个子线程 t1 和 t2 之后, 就一直检测 t2 是否还活着,如果 t2 活着就接着检测 t1 是否活着。当 t2 活着而 t1 已经结束, 则将一个用作退出标志的全局变量 exitFlag 设置为 True(初始值为 False)。而 t2 执行任务循 环时会检测 exitFlag 变量,一旦发现它变成了 True,就知道另一个线程已经结束,于是不再 执行后面的任务,直接结束返回。读者应该注意到这件事的意义,它意味着多个线程之间是 可以进行协作、同步的,而不是各自只管闷着头做自己的事情。
以下是程序 9.6 的一次执行结果:
```
<老孙>:我是孙悟空!
<老孙>:我拔根毫毛变个小猴儿<哼>
<哼>: 上场...
<老孙>:我再拔根毫毛变个小猴儿<哈>
<哈>: 上场...
<哼>:0
<哼>:1
<哈>:0
<哼>:2
<哈>:1
<哼>:3
<哼>:4
<哼>: 退场...
<哈>:2
<哼>已退场,<哈>也提前结束吧~~~
<哈>: 退场...
<老孙>:小猴儿都回了,俺老孙去也~~~
>>>
```
线程有名字,可通过 getName()和 setName()方法读出或设置线程名。 总是有一个主线程对象,它对应于程序的初始控制流。
并发计算中的同步问题
多个线程之间如果只是彼此独立地执行各自的任务,事情就简单了。但是实际应用中常常需要多个线程之间进行合作,合作的线程往往要存取一些公共数据。由于多个线程的执行 顺序是不可预测的,这就有可能导致公共数据处于不一致的状态。因此,如果允许多个线程 并发读写公共数据,就必须对多线程的交互进行控制,以保护公共数据的正确性。
程序 9.6 演示了两个线程通过一个全局变量进行协同的例子。
又如,Thread 对象具有一个 join()方法,一个线程对象 t1 可以调用另一个线程对象 t2 的 join()方法,这导致 t1 暂停执行,直至 t2 执行结束(或者执行一个指定的时间)。可见, join 方法能实现让一个线程等待另一个线程以便进行某种同步的目的。
threading 模块还实现了更一般的同步机制,在此就不介绍了。
- 前言
- 第 1 章 计算与计算思维
- 1.1 什么是计算?
- 1.1.1 计算机与计算
- 1.1.2 计算机语言
- 1.1.3 算法
- 1.1.4 实现
- 1.2 什么是计算思维?
- 1.2.1 计算思维的基本原则
- 1.2.2 计算思维的具体例子
- 1.2.3 日常生活中的计算思维
- 1.2.4 计算思维对其他学科的影响
- 1.3 初识 Python
- 1.3.1 Python 简介
- 1.3.2 第一个程序
- 1.3.3 程序的执行方式
- 1.3.4 Python 语言的基本成分
- 1.4 程序排错
- 1.5 练习
- 第 2 章 用数据表示现实世界
- 2.1 数据和数据类型
- 2.1.1 数据是对现实的抽象
- 2.1.1 常量与变量
- 2.1.2 数据类型
- 2.1.3 Python 的动态类型*
- 2.2 数值类型
- 2.2.1 整数类型 int
- 2.2.2 长整数类型 long
- 2.2.3 浮点数类型 float
- 2.2.4 数学库模块 math
- 2.2.5 复数类型 complex*
- 2.3 字符串类型 str
- 2.3.1 字符串类型的字面值形式
- 2.3.2 字符串类型的操作
- 2.3.3 字符的机内表示
- 2.3.4 字符串类型与其他类型的转换
- 2.3.5 字符串库 string
- 2.4 布尔类型 bool
- 2.4.1 关系运算
- 2.4.2 逻辑运算
- 2.4.3 布尔代数运算定律*
- 2.4.4 Python 中真假的表示与计算*
- 2.5 列表和元组类型
- 2.5.1 列表类型 list
- 2.5.2 元组类型 tuple
- 2.6 数据的输入和输出
- 2.6.1 数据的输入
- 2.6.2 数据的输出
- 2.6.3 格式化输出
- 2.7 编程案例:查找问题
- 2.8 练习
- 第 3 章 数据处理的流程控制
- 3.1 顺序控制结构
- 3.2 分支控制结构
- 3.2.1 单分支结构
- 3.2.2 两路分支结构
- 3.2.3 多路分支结构
- 3.3 异常处理
- 3.3.1 传统的错误检测方法
- 3.3.2 传统错误检测方法的缺点
- 3.3.3 异常处理机制
- 3.4 循环控制结构
- 3.4.1 for 循环
- 3.4.2 while 循环
- 3.4.3 循环的非正常中断
- 3.4.4 嵌套循环
- 3.5 结构化程序设计
- 3.5.1 程序开发过程
- 3.5.2 结构化程序设计的基本内容
- 3.6 编程案例:如何求 n 个数据的最大值?
- 3.6.1 几种解题策略
- 3.6.2 经验总结
- 3.7 Python 布尔表达式用作控制结构*
- 3.8 练习
- 第 4 章 模块化编程
- 4.1 模块化编程基本概念
- 4.1.1 模块化设计概述
- 4.1.2 模块化编程
- 4.1.3 编程语言对模块化编程的支持
- 4.2 Python 语言中的函数
- 4.2.1 用函数减少重复代码 首先看一个简单的用字符画一棵树的程序:
- 4.2.2 用函数改善程序结构
- 4.2.3 用函数增强程序的通用性
- 4.2.4 小结:函数的定义与调用
- 4.2.5 变量的作用域
- 4.2.6 函数的返回值
- 4.3 自顶向下设计
- 4.3.1 顶层设计
- 4.3.2 第二层设计
- 4.3.3 第三层设计
- 4.3.4 第四层设计
- 4.3.5 自底向上实现与单元测试
- 4.3.6 开发过程小结
- 4.4 Python 模块*
- 4.4.1 模块的创建和使用
- 4.4.2 Python 程序架构
- 4.4.3 标准库模块
- 4.4.4 模块的有条件执行
- 4.5 练习
- 第 5 章 图形编程
- 5.1 概述
- 5.1.1 计算可视化
- 5.1.2 图形是复杂数据
- 5.1.3 用对象表示复杂数据
- 5.2 Tkinter 图形编程
- 5.2.1 导入模块及创建根窗口
- 5.2.2 创建画布
- 5.2.3 在画布上绘图
- 5.2.4 图形的事件处理
- 5.3 编程案例
- 5.3.1 统计图表
- 5.3.2 计算机动画
- 5.4 软件的层次化设计:一个案例
- 5.4.1 层次化体系结构
- 5.4.2 案例:图形库 graphics
- 5.4.3 graphics 与面向对象
- 5.5 练习
- 第 6 章 大量数据的表示和处理
- 6.1 概述
- 6.2 有序的数据集合体
- 6.2.1 字符串
- 6.2.2 列表
- 6.2.3 元组
- 6.3 无序的数据集合体
- 6.3.1 集合
- 6.3.2 字典
- 6.4 文件
- 6.4.1 文件的基本概念
- 6.4.2 文件操作
- 6.4.3 编程案例:文本文件分析
- 6.4.4 缓冲
- 6.4.5 二进制文件与随机存取*
- 6.5 几种高级数据结构*
- 6.5.1 链表
- 6.5.2 堆栈
- 6.5.3 队列
- 6.6 练习
- 第 7 章 面向对象思想与编程
- 7.1 数据与操作:两种观点
- 7.1.1 面向过程观点
- 7.1.2 面向对象观点
- 7.1.3 类是类型概念的发展
- 7.2 面向对象编程
- 7.2.1 类的定义
- 7.2.2 对象的创建
- 7.2.3 对象方法的调用
- 7.2.4 编程实例:模拟炮弹飞行
- 7.2.5 类与模块化
- 7.2.6 对象的集合体
- 7.3 超类与子类*
- 7.3.1 继承
- 7.3.2 覆写
- 7.3.3 多态性
- 7.4 面向对象设计*
- 7.5 练习
- 第 8 章 图形用户界面
- 8.1 图形用户界面概述
- 8.1.1 程序的用户界面
- 8.1.2 图形界面的组成
- 8.1.3 事件驱动
- 8.2 GUI 编程
- 8.2.1 UI 编程概述
- 8.2.2 初识 Tkinter
- 8.2.3 常见 GUI 构件的用法
- 8.2.4 布局
- 8.2.5 对话框*
- 8.3 Tkinter 事件驱动编程
- 8.3.1 事件和事件对象
- 8.3.2 事件处理
- 8.4 模型-视图设计方法
- 8.4.1 将 GUI 应用程序封装成对象
- 8.4.2 模型与视图
- 8.4.3 编程案例:汇率换算器
- 8.5 练习
- 第 9 章 模拟与并发
- 9.1 模拟
- 9.1.1 计算机建模
- 9.1.2 随机问题的建模与模拟
- 9.1.3 编程案例:乒乓球比赛模拟
- 9.2 原型法
- 9.3 并行计算*
- 9.3.1 串行、并发与并行
- 9.3.2 进程与线程
- 9.3.3 多线程编程的应用
- 9.3.4 Python 多线程编程
- 9.3.5 小结
- 9.4 练习
- 第 10 章 算法设计和分析
- 10.1 枚举法
- 10.2 递归
- 10.3 分治法
- 10.4 贪心法
- 10.5 算法分析
- 10.5.1 算法复杂度
- 10.5.2 算法分析实例
- 10.6 不可计算的问题
- 10.7 练习
- 第 11 章 计算+X
- 11.1 计算数学
- 11.2 生物信息学
- 11.3 计算物理学
- 11.4 计算化学
- 11.5 计算经济学
- 11.6 练习
- 附录
- 1 Python 异常处理参考
- 2 Tkinter 画布方法
- 3 Tkinter 编程参考
- 3.1 构件属性值的设置
- 3.2 构件的标准属性
- 3.3 各种构件的属性
- 3.4 对话框
- 3.5 事件
- 参考文献