本文首發於知乎
多執行緒理解
多執行緒是多個任務同時執行的一種方式。比如一個迴圈中,每個迴圈看做一個任務,我們希望第一次迴圈執行還沒結束時,就可以開始第二次迴圈,用這種方式來節省時間。
python中這種同時執行的目的是最大化利用CPU的計算能力,將很多等待時間利用起來。這也說明如果程式耗時不是因為等待時間,而是任務非常多,就是要計算那麼久,則多執行緒無法改善執行時間。
更多有關多執行緒理解的內容可以參考下面資料
簡單使用
先看下面這個函式
import time
def myfun():
time.sleep(1)
a = 1 + 1
print(a)
複製程式碼
如果我們要執行10次這個函式,它的執行時間主要在於每次sleep
的那一秒,1 + 1
的計算是不會耗多少時間的。這種情況可以用多執行緒提高效率。
下面來看一下不使用多執行緒耗時和使用多執行緒的耗時
不使用多執行緒
t = time.time()
for _ in range(5):
myfun()
print(time.time() - t)
複製程式碼
得到結果是 5.002434492111206
下面我們使用多執行緒
from threading import Thread
for _ in range(5):
th = Thread(target = myfun)
th.start()
複製程式碼
這樣使用多執行緒其實就可以了,你會發現大概1秒,5個2
會同時出來,說明5次迴圈其實幾乎同時執行,5次1秒的等待時間同時進行,最後只等待了1秒。
這裡多執行緒只包括了兩步
- 用
Thread
增加一個執行緒,這裡是將每一次迴圈作為一次新的執行緒,一個執行緒執行一次myfun
函式。 - 用
start()
開始執行這個執行緒,每個執行緒都需要這樣顯式開啟才會執行。一個執行緒這樣開啟後,不需要等待它執行完成,就可以繼續執行下面的程式,即下一次迴圈(然後又新建了第二個執行緒,執行未結束即開啟第三個……)
這裡要注意一點:多執行緒是放在迴圈裡面的,不能定義好迴圈之後,從外面將它變成多執行緒。
讀者可能會注意到,不用多執行緒時是通過程式計算時間的,使用多執行緒卻沒有。這是因為要計算時間需要增加一些程式碼,無法展示最簡單的多執行緒使用,所以就先不計算時間。接下來我們就講join()
的使用,並計算時間。
join的使用
執行緒的join()
方法表示等這個執行緒執行完畢,程式再往下執行。我們來看下面的例子
from threading import Thread
t = time.time()
for _ in range(5):
th = Thread(target = myfun)
th.start()
th.join()
print(time.time() - t)
# 結果為 5.0047078132629395 秒
複製程式碼
這裡start()
之後馬上join()
,表示每一個執行緒都要執行結束才能進行下一次迴圈,這樣就和沒有使用多執行緒沒有區別了。不過如果要計算多執行緒執行時間卻是要用到這個join()
我們先看一下不用join()
的情況
from threading import Thread
t = time.time()
for _ in range(5):
th = Thread(target = myfun)
th.start()
print(time.time() - t)
# 結果為 0.0009980201721191406 秒
複製程式碼
它連1秒都沒有等,就輸出了結果,而且5個2是在列印出這個之後才輸出出來的。這是因為print(time.time() - t)
是區別於那5次迴圈執行緒之外的第6個執行緒,它不會等待5個執行緒執行結束就會開始執行。所以這樣是無法獲得上面5個執行緒的執行時間的,我們需要用join()
等待5個執行緒都執行結束。
程式碼如下
from threading import Thread
t = time.time()
ths = []
for _ in range(5):
th = Thread(target = myfun)
th.start()
ths.append(th)
for th in ths:
th.join()
print(time.time() - t)
# 結果為 1.0038363933563232
複製程式碼
上面定義ths
列表儲存這些執行緒,最後用迴圈確保每一個執行緒都已經執行完成再計算時間差。
join()
不只是用於這種情形。當一步程式碼執行依賴之前程式碼執行完成時,就要加入join()
命令。
現在我們已經學完了多執行緒的一般使用方法,可以在多數場景使用了。下面來介紹一些細節
其他
(1)執行緒名稱
我們直接看下面的程式碼
import threading
print(threading.current_thread().getName())
def myfun():
time.sleep(1)
print(threading.current_thread().name)
a = 1 + 1
for i in range(5):
th = threading.Thread(target = myfun, name = 'thread {}'.format(i))
th.start()
# 輸出結果
MainThread
thread 0
thread 1
thread 4
thread 3
thread 2
複製程式碼
解釋一下
threading.current_thread()
表示當前執行緒,可以呼叫name
或getName()
獲取執行緒名稱- 任何程式都會預設啟動一個執行緒,預設名稱為
MainThread
,也就是主程式佔一個執行緒,這個執行緒和之後用Thread
新加的執行緒是相互獨立的,主執行緒不會等待其餘執行緒執行結束就會繼續往下執行。之前不用join()
無法計算執行時間就是因為主執行緒先執行完了。 Thread
表示執行這個函式啟動一個新的執行緒,在其中加一個name
引數指定這個函式執行緒名,則在這個函式內列印執行緒名就顯示這裡name
引數對應值- 在迴圈中列印有兩種。第一種
print(threading.current_thread().name)
則是MainThread
;第二種print(th.name)
則是thread 1
等
(2)Thread函式
上面我們使用了Thread函式的target name
引數,下面來說一下它的其他引數
args
指定target
對應函式的引數,用元組傳入,比如args = (3, )
daemon
主執行緒預設是False
,如果沒有指定則繼承父執行緒的值。True
則如果主執行緒執行結束,該執行緒也停止執行;False
則該執行緒會繼續執行直到執行結束,無視主執行緒如何。(要看這個引數的效果要在py檔案中編寫程式碼,在cmd裡執行,不能在jupyter notebook裡,因為這裡會多出一些執行緒干擾)group
是預留的一個引數,用於以後擴充套件ThreadGroup
類,現在沒用
(3)Thread物件
上面threading.Thread
和threading.current_thread()
都建立了一個Thread物件,Thread物件有如下屬性和方法
getName() .name
獲取執行緒名setName()
設定執行緒名start() join()
這兩個之前說過了join()
有一個timeout
引數,表示等待這個執行緒結束時,如果等待時間超過這個時間,就不再等,繼續進行下面的程式碼,但是這個執行緒不會被中斷run()
也是執行這個執行緒,但是必須等到這個執行緒執行結束才會繼續執行之後的程式碼(如果將上面的start
全換成run
則相當於沒有開多執行緒)is_alive()
如果該執行緒還沒執行完,就是True
否則False
daemon
返回該執行緒的daemon
setDaemon(True)
設定執行緒的daemon
(4)threading
一些直接呼叫的變數
threading.currentThread()
: 返回當前的執行緒變數threading.enumerate()
: 返回一個包含正在執行的執行緒的listthreading.activeCount()
: 返回正在執行的執行緒數量,與len(threading.enumerate())
有相同的結果
歡迎關注我的知乎專欄
專欄主頁:python程式設計
專欄目錄:目錄
版本說明:軟體及包版本說明