threading 模塊的 Thread 類的使用
1. 多線程的基本概念
程序要完成兩個任務:
- 任務 1 進行一項復雜的計算,需要 1 秒才能完成。
- 任務 2 讀取磁盤,需要 1 秒才能完成。
我們可以串行的執行這兩項任務,先執行任務 1,再執行任務 2,完成這兩項任務總共需要 2 秒,如下圖所示:
我們可以并行的執行這兩項任務,同時執行這兩項任務,完成這兩項任務只需要 1 秒,如下圖所示:
顯然,并行執行的時間小于串行執行的時間。很多場景下,我們希望程序能夠同時執行多個任務,操作系統提供了多線程的機制用于實現并行執行多個任務。在操作系統中,線程是一個可以獨立執行的任務。程序執行時至少包含一個線程,可以使用線程相關的 API 創建新的線程。
Python 的 threading 模塊提供了類 Thread,用戶通過新建一個類 Thread 創建新的線程,本文描述了類 Thread 的基本使用。
2. 多線程的基本使用
Python 的 threading 模塊中提供了類 Thread 用于實現多線程,用戶有兩種使用多線程的方式:
- 在線程構造函數中指定線程的入口函數。
- 自定義一個類,該類繼承類 Thread,在自定義的類中實現 run 方法。
2.1 線程的構造函數和重要的成員方法
本節介紹 Thread 相關的三個函數的功能:
- 類 Thread 的構造函數
- 類 Thread 的 start 方法
- 類 Thread 的 join 方法
2.1.1 類Thread的構造函數
Thread(group = None, target = None, name = None, args = (), kwargs = {})
參數的含義如下:
- group: 線程組,目前還沒有實現,在此處必須是 None。
- target: 線程的入口函數,線程從該函數開始執行。
- name: 線程名。
- args: 線程的入口函數的參數,以元組的形式傳入。
- kwargs: 線程的入口函數的參數,以字典的形式傳入。
使用 Thread 構造一個新線程時,必須指定 target 和 args 兩個參數,target 為線程的入口,args 為線程入口函數的參數。
2.1.2 類 Thread 的 start 方法
start()
在線程對象的構造函數中 target 指定了線程入口函數,args 指定了線程入口函數的參數。線程對象的 start 方法使新線程開始執行,執行函數 target(args)。
2.1.3 類 Thread 的 join 方法
join()
調用線程對象的 start 方法后,新線程開始執行函數 target(args)。調用線程對象的 join 方法,主線程阻塞,等待新線程執行完畢。
2.2 指定線程的入口函數
下面通過一個具體的例子,說明通過指定線程的入口函數的方式使用多線程。
import time
import threading
def thread_entry(begin, end):
for i in range(begin, end):
time.sleep(1)
print(i)
t0 = threading.Thread(target = thread_entry, args = (1, 4))
t1 = threading.Thread(target = thread_entry, args = (101, 104))
t0.start()
t1.start()
t0.join()
t1.join()
-
在第 9 行和第 10 行,通過調用 Thread 的構造函數創建了兩個線程。
-
在第 9 行,設定線程的入口函數為 thread_entry,傳遞給入口函數兩個參數:1 和 4,新的線程將執行 thread_entry(1, 4),變量 t0 指向新創建的線程對象。
-
在第 10 行,設定線程的入口函數為 thread_entry,傳遞給入口函數兩個參數:101 和 104,新的線程將執行 thread_entry(101, 104),變量 t1 指向新創建的線程對象。
-
在第 4 行到第 7 行,定義了線程入口函數,該函數的功能是打印在 [begin, end) 區間的整數,每打印一個整數,調用 time.sleep(1) 睡眠 1 秒鐘。
-
在第 11 行,調用 start 方法啟動線程 t0,t0 開始執行 thread_entry(1, 4)。
-
在第 12 行,調用 start 方法啟動線程 t1,t1 開始執行 thread_entry(101, 104)。
-
在第 13 行和第 14 行,調用 join 方法,等待線程 t0 和 t1 執行完畢。
程序的運行結果如下:
1
101
2
102
3
103
線程 t0 的輸出結果為 1、2、3,線程 t1 的輸出結果為 101、102、103。由于兩者是并發執行的,所以結果交織在一起。
2.3 繼承 Thread
下面通過一個具體的例子,說明通過繼承 Thread 的方式使用多線程。
import time
import threading
class MyThread(threading.Thread):
def __init__(self, begin, end):
threading.Thread.__init__(self)
self.begin = begin
self.end = end
def run(self):
for i in range(self.begin, self.end):
time.sleep(1)
print(i)
t0 = MyThread(1, 4)
t1 = MyThread(101, 104)
t0.start()
t1.start()
t0.join()
t1.join()
-
在第 4 行,定義類 MyThread,繼承 threading.Thread。
-
在第 5 行,定義了構造函數 __init__,首先調用父類 thread.Thread.__init__ 初始化 Thread 對象,然后將參數 begin 和 end 保存在 MyThread 的成員變量中。
-
在第 10 行,定義了方法 run,當線程開始運行時,run 方法會被調用。在 run 方法中,打印在 [begin, end) 區間的整數,每打印一個整數,調用 time.sleep(1) 睡眠 1 秒鐘。
-
在第 15 行和第 16 行,通過調用 MyThread 的構造函數創建了兩個線程。
-
在第 17 行,調用 start 方法啟動線程 t0,t0 開始執行 MyThread 的方法 run()。
-
在第 18 行,調用 start 方法啟動線程 t1,t1 開始執行 MyThread 的方法 run()。
-
在第 19 行和第 20 行,調用 join 方法,等待線程 t0 和 t1 執行完畢。
程序的運行結果如下:
1
101
2
102
3
103
線程 t0 執行 thread_entry(1, 4),輸出結果為 1、2、3,線程 t1 執行 thread_entry(101, 104),輸出結果為 101、102、103。由于兩者是并發執行的,所以結果交織在一起。
2.4 常見的錯誤
2.4.1 自定義的類的 __init__ 方法忘記調用父類 Thread 的 __init__ 方法
通過自定義類繼承 Thread 的方式實現線程時,要求自定義的類的 __init__ 方法調用父類 Thread 的 __init__ 方法,如果忘記調用 Thread 的 __init__ 方法,則會報錯。編寫 forget_init.py,其內容如下:
import time
import threading;
class MyThread(threading.Thread):
def __init__(self, id):
# 在此處沒有調用父類 threading.Thread.__init__ 方法
self.id = id
def run(self):
for i in range(3):
print('This is thread %s' % self.id)
time.sleep(3)
t1 = MyThread(0)
t1 = MyThread(1)
t0.start()
t1.start()
t0.join()
t1.join()
運行 forget_init.py,程序輸出如下:
Traceback (most recent call last):
File "forget_init.py", line 14, in <module>
t0 = MyThread(0)
File "forget_init.py", line 7, in __init__
self.id = id
File "/usr/lib/python3.6/threading.py", line 1089, in name
assert self._initialized, "Thread.__init__() not called"
AssertionError: Thread.__init__() not called
以上錯誤信息顯示,Thread.__init__ 沒有被調用。
2.4.2 只有一個線程參數時,使用 (arg) 表示線程參數
元組只包含一個元素時,必須加一個逗號,在下面的定義中,變量 tuple 表示的是一個元組,該元組包含了一個元素 123。
>>> tuple = (123,)
>>> tuple
(123,)
在下面的定義中,忘記加逗號,則變量 expression 表示的是一個整數類型的表達式,變量 expression 是一個整數 123,而不是元組 (123,)。
>>> expression = (123)
>>> expression
123
通過指定線程入口函數的方式實現線程時,使用元組傳遞線程參數,如果只有一個線程參數 arg,使用 (arg) 表示線程參數時,則會報錯。編寫程序 not_tuple.py,內容如下:
import time
import threading;
def run(id):
for i in range(3):
print('This is thread %d' % id)
time.sleep(3)
t0 = threading.Thread(target = run, args = (0)) # 此處錯誤,應為(0,)
t1 = threading.Thread(target = run, args = (1)) # 此處錯誤,應為(1,)
t0.start()
t1.start()
t0.join()
t1.join()
運行 not_tuple.py,程序輸出如下:
Exception in thread Thread-1:
Traceback (most recent call last):
File "/usr/lib/python3.6/threading.py", line 916, in _bootstrap_inner
self.run()
File "/usr/lib/python3.6/threading.py", line 864, in run
self._target(*self._args, **self._kwargs)
TypeError: run() argument after * must be an iterable, not int
以上顯示錯誤信息 “TypeError: run() argument after * must be an iterable, not int”,初學者很難看明白這段錯誤信息,這段錯誤信息表示 run() 的 arguments 必須是可以遍歷的(iterable)。線程入口參數是一個元組,而參數 (0) 表示的是一個整數而不是元組 (0,)。
3. 使用多線程進行并行 IO 操作
本節通過實例說明 Python 多線程的使用場景?,F在需要編寫程序獲取 baidu.com、taobao.com、qq.com 首頁,程序包括 3 個任務:
- 獲取 baidu.com 的首頁
- 獲取 taobao.com 的首頁
- 獲取 qq.com 的首頁
本節需要使用到 python 的 requests 模塊,requests 模塊的用于 http 請求,requests 模塊提供了 get 方法用于獲取網頁。
在 3.1 小節演示串行執行這 3 個任務,并記錄串行完成 3 個任務總共所需要的時間;在 3.2 小節演示并行執行這 3 個任務,并記錄并行完成 3 個任務總共所需要的時間。
3.1 串行獲取 baidu.com、taobao.com、qq.com 首頁
編寫程序 serial.py,該程序以串行的方式獲取 baidu、taobao、qq 的首頁,內容如下:
from datetime import datetime
import requests
import threading
def fetch(url):
response = requests.get(url)
print('Get %s: %s' % (url, response))
time0 = datetime.now()
fetch("https://www.baidu.com/")
fetch("https://www.taobao.com/")
fetch("https://www.qq.com/")
time1 = datetime.now()
time = time1 - time0
print(time.microseconds)
-
在第 5 行,定義了函數 fetch,函數 fetch 獲取指定 url 的網頁。
-
在第 6 行,調用 requests 模塊的 get 方法獲取獲取指定 url 的網頁。
-
在第 9 行,記錄執行的開始時間。
-
在第 11 行到第 13 行,串行執行獲取 baidu、taobao、qq 的首頁。
-
在第 15 行到第 17 行,記錄執行的結束時間,并計算總共花費的時間,time.micoseconds 表示完成需要的時間(微秒)。
執行 serial.py,輸出如下:
Get https://www.baidu.com/: <Response [200]>
Get https://www.taobao.com/: <Response [200]>
Get https://www.qq.com/: <Response [200]>
683173
在輸出中,<Response [200]> 是服務器返回的狀態碼,表示獲取成功。成功獲取了 baidu、taobao、qq 的首頁,總共用時為 683173 微秒。
3.2 并行獲取 baidu.com、taobao.com、qq.com 首頁
編寫程序 parallel.py,該程序以并行的方式獲取 baidu、taobao、qq 的首頁,內容如下:
from datetime import datetime
import requests
import threading
def fetch(url):
response = requests.get(url)
print('Get %s: %s' % (url, response))
time0 = datetime.now()
t0 = threading.Thread(target = fetch, args = ("https://www.baidu.com/",))
t1 = threading.Thread(target = fetch, args = ("https://www.taobao.com/",))
t2 = threading.Thread(target = fetch, args = ("https://www.qq.com/",))
t0.start()
t1.start()
t2.start()
t0.join()
t1.join()
t2.join()
time1 = datetime.now()
time = time1 - time0
print(time.microseconds)
-
在第 5 行,定義了函數 fetch,函數 fetch 獲取指定 url 的網頁。
-
在第 6 行,調用 requests 模塊的 get 方法獲取獲取指定 url 的網頁。
-
在第 9 行,記錄執行的開始時間。
-
在第 11 行到第 13 行,創建了 3 個線程,分別執行獲取 baidu、taobao、qq 的首頁。
-
在第 14 行到第 16 行,啟動這 3 個線程,這 3 個線程并行執行。
-
在第 17 行到第 19 行,等待這 3 個線程執行完畢。
-
在第 21 行到第 23 行,記錄執行的結束時間,并計算總共花費的時間,time.micoseconds 表示完成需要的時間(微秒)。
執行 parallel.py,輸出如下:
Get https://www.baidu.com/: <Response [200]>
Get https://www.qq.com/: <Response [200]>
Get https://www.taobao.com/: <Response [200]>
383800
在輸出中,<Response [200]> 是服務器返回的狀態碼,表示獲取成功。成功獲取了 baidu、taobao、qq的首頁,總共用時為 383800 微秒。相比執行,串行執行總共用時為 683173 微秒,因此使用多線程加快了程序的執行速度。
4. 獲取線程的返回值
在繼承 Thread 實現多線程的方式中,將線程的返回值保存在線程對象中,使用一個成員變量保存線程的返回值。下面通過一個具體的例子,說明如何獲取線程的返回值。使用多線程技術計算 1+2+3 … + 100 的累加和,算法思路如下:
-
主程序創建 2 個線程:
-
線程 1,計算前 50 項的累加和,即 1+2+3 … + 50,保存計算結果。
-
線程 2,計算后 50 項的累加和,即 51+52+53 … + 100,保存計算結果。
-
主程序等待線程 1 和線程 2 執行完畢,獲取它們各自的計算結果,并相加得到最終的計算結果。
編寫程序 get_return_value.py,其內容如下:
import threading
class MyThread(threading.Thread):
def __init__(self, begin, end):
threading.Thread.__init__(self)
self.begin = begin
self.end = end
def run(self):
self.result = 0
for i in range(self.begin, self.end):
self.result += i
t0 = MyThread(1,51)
t1 = MyThread(51,101)
t0.start()
t1.start()
t0.join()
t1.join()
print(t0.result)
print(t1.result)
print(t0.result + t1.result)
-
在第 14 行,創建第一個線程,計算區間 [1, 51) 內的累加和。
-
在第 15 行,創建第二個線程,計算區間 [51, 101) 內的累加和。
-
在第 4 行,函數 __init__ 將線程參數 begin 和 end 保存到線程對象中。
-
在第 9 行,線程啟動后執行函數 run。
-
在第 10 行到第 12 行,使用 self.result 保存線程的計算結果。
-
在第 16 行到第 19 行,啟動線程進行計算,主程序等待子線程計算結束。
-
在第 20 行到第 22 行,從 t0.result 中獲取線程 t0 的計算結果,從 t1.result 中獲取線程 t1 的計算結果,將兩者相加,打印最終的結果。
運行程序 get_return_value.py,輸出如下:
1275
3775
5050
線程 t0 計算前 50 項,計算結果為 1275;線程 t1 計算后 50 項,計算結果為 3775;主程序將兩者相加,得到最終的計算結果為 5050。