### 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
① 在作者所用的計算機平臺(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 事件
- 參考文獻