python多執行緒基礎

dwzb發表於2018-03-08

本文首發於知乎

多執行緒理解

多執行緒是多個任務同時執行的一種方式。比如一個迴圈中,每個迴圈看做一個任務,我們希望第一次迴圈執行還沒結束時,就可以開始第二次迴圈,用這種方式來節省時間。

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()表示當前執行緒,可以呼叫namegetName()獲取執行緒名稱
  • 任何程式都會預設啟動一個執行緒,預設名稱為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.Threadthreading.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(): 返回一個包含正在執行的執行緒的list
  • threading.activeCount(): 返回正在執行的執行緒數量,與len(threading.enumerate())有相同的結果

歡迎關注我的知乎專欄

專欄主頁:python程式設計

專欄目錄:目錄

版本說明:軟體及包版本說明

相關文章