Python 官方文檔解讀(2):threading 模塊

使用 Python 可以編寫多線程程序,注意,這並不是說程序能在多個 CPU 核上跑。如果你想這麼做,可以看看關於 Python 並行計算的,比如官方 Wiki

Python 線程的主要應用場景是一些包含等待或 I/O 的任務,比如與遠程 Web 服務器交互,多線程能夠讓 Python 在等待時執行其他代碼,這提高了系統的交互性。例如下面這個爬蟲程序:

import Queue
import threading
import urllib2

# 被每個線程調用
def get_url(q, url):
    q.put(urllib2.urlopen(url).read())

urls = ["http://google.com", "http://yahoo.com"]

q = Queue.Queue()

for u in urls:
    t = threading.Thread(target=get_url, args = (q,u))
    t.daemon = True
    t.start()

s = q.get()
print(s)

這個程序會幾乎同時發出對 Google 和 Yahoo 的 HTTP 請求,並輸出最先獲得響應的網頁。

該模塊的設計基於 Java 的線程模型。但是,在 Java 使鎖和條件變量成爲每個對象的基本行爲的地方,它們是 Python 中的獨立對象。Python 的 Thread 類支持 Java Thread 類的行爲的子集;目前在 Python 中,沒有支持優先級,沒有線程組,線程不能被銷燬、停止、暫停、恢復或中斷。 Java 的 Thread 類的靜態方法在被映射到模塊級全局函數。

本模塊中的所有方法都是原子執行的 (atomically)。

函數

threading 模塊定義了以下函數:

active_count()

返回當前處於生存狀態 (alive) 的 Thread 對象的數量,它和 enumerate() 所返回的列表的長度相同。

current_thread()

返回當前的 Thread 對象。如果當前線程不是通過 threading 模塊創建的,那麼會返回一個功能受限的“啞對象” (Dummy object)。

get_ident()

返回當前線程的線程標識符,它是一個非零整數,沒有實際意義,但你可以用它來索引一個線程相關數據結構(例如用一個全局列表存儲當前程序中的所有線程)。注意:當線程被回收和創建時,這些標識符可能會被回收重用。

enumerate()

返回一個當前 alive 的線程對象的列表。

main_thread()

返回主線程對象,就是 Python 解釋器啓動時的線程。

settrace(func)

爲所有由 threading 模塊啓動的線程設置一個 trace 函數 func 。在每個線程的 run() 之前,這個 func 會被傳給 sys.settrace() 爲每個線程設置 trace 函數。

setprofile(func)

爲所有由 threading 模塊啓動的線程設置一個 profile 函數 func 。在每個線程的 run() 之前,這個 func 會被傳給 sys.setprofile() 爲每個線程設置 profile 函數。

stack_size([size])

設置或返回之後創建新線程時的 stack 大小。size 可以是 0 (使用系統默認值)或大於等於 32,768 (32 KiB 是 Python 解釋器能夠施展拳腳的最小棧空間).

常量

TIMEOUT_MAX

阻塞函數的 timeout 參數的允許最大值。阻塞函數包括 Lock.acquire(), RLock.acquire(), Condition.wait() 等。

Thread-Local 數據

class local()

Thread-local 數據是專屬於某個線程的局部數據。它在 global scope 被定義,但如果在某個線程裏訪問它,它是局部特有的。示例查看 _threading_local 模塊。

Thread

Thread 類表示在單獨的控制線程中運行的活動。有兩種方法來指定這種活動:通過將 callable 對象傳遞給構造函數,或者通過覆蓋子類中的 run() 方法。換句話說,你只能覆蓋這個類的 __init__()run() 方法。

創建 Thread 對象後,通過調用 start() 方法來啓動這個線程,它會自動調用 run() 方法。

當這個線程啓動後,它的狀態轉爲 alive。當它的 run() 終止或出現了沒有處理的異常時,它的狀態轉爲非 alive。可以用 is_alive() 函數來查看一個線程的狀態。

其他線程可以調用某個線程的 join() 方法,這會阻塞調用方的線程,直到被調用方的線程終止。

一個線程可以有名字,你可以讀/寫 name 屬性。

一個線程可以被標記爲 daemon 線程,這種線程在主線程退出後就會自動退出,所以你不需要手動關閉它們。

有可能會出現“啞線程對象”,或者叫做“外星線程”,指代那些不是由 threading 模塊創建的線程,比如從 C 代碼創建的。它們無法被 join(),因爲無法探測它們何時終止。

class Thread(group=None, target=None, name=None, args=(), kwargs={}, **, daemon=None*)

  • group 必須是 None,這是爲以後 Python 實現線程組做準備的。

  • target 是一個可調用對象,被 run() 調用。

  • name 是線程名,默認會被設置爲 "Thread-N",N 爲整數。

  • args 是傳入 target 的參數,是一個元組,默認爲 ()

  • kwargs 是傳入 target 的關鍵字參數,是一個字典,默認爲 {}

  • daemon 設置此線程是否爲守護線程。默認 None 意爲從本線程繼承。

start()

啓動線程。最多被一個線程調用一次。如果被多次調用,會引發 RuntimeError

run()

代表線程活動的函數。

join(timeout=None)

等待一個線程的終止。由於這個函數總是返回 None,所以如果你傳入了 timeout,那麼之後還需要用 is_alive() 來判斷這個線程是正常終止還是超時了。

如果 join() 被用在了本線程上,就會引發 RuntimeError,因爲這會引起死鎖。把它用在一個還沒有啓動的線程上同樣會引發這個異常。

name

線程名字,沒有實際語義。舊的 API 有 getName()setName,但沒必要使用了。

ident

線程描述符,通過 get_ident() 來訪問。

is_alive()

返回線程是否還在運行。

daemon

線程是否爲守護線程。舊的 API 有 isDaemon()setDaemon(),也沒必要使用了。

CPython 實現細節:在 CPython 中,由於 GIL 的存在,在同一時刻僅有一個線程能運行。因此 Python threading 模塊的主要應用場景是同時運行多個 I/O 密集型的任務。如果你想利用多核計算資源,可以使用多進程模塊 multiprocessingconcurrent.futures.ProcessPoolExecutor

Lock

原始鎖是一種同步原語,在鎖定時不屬於特定線程。在 Python 中,它是目前可用的最低級別同步原語,由 _thread 擴展模塊直接實現。

一個原始鎖有兩個主要的方法:acquire()release(),分別用來上鎖和解鎖。如果嘗試 release 一個沒有鎖上的鎖,會引發 RuntimeError。原始鎖支持 “上下文管理協議(Context Management Protocol)”。

上下文管理協議”:使用 with 語句來獲得一個鎖、條件變量或信號量,相當於調用 acquire();離開 with 塊後,會自動調用 release()。例如:

with lock:  # 如果無法獲取則會阻塞在這裏
    # 在這裏鎖已經被獲得
# 在外面鎖被釋放

如果有多個線程在等待同一個鎖,當這個鎖被釋放時,哪一個進程會獲得鎖是不確定的,這取決於實現。

class Lock

返回一個原始鎖。此類的方法有:

acquire(blocking=True, timeout=-1)

獲得一個鎖,可以是阻塞或非阻塞的。默認爲阻塞。也可以設置 timeout 來設置阻塞的最長時間。

返回 TrueFalse 告訴用戶是否成功獲得鎖。

release()

釋放一個鎖。沒有返回值。

RLock

可重入鎖 (Reetrant Lock) 是一種同步原語,與原始鎖的唯一區別是可以由同一線程多次獲取。在內部,除了原始鎖使用的鎖定/解鎖狀態之外,它還使用“擁有線程”和“遞歸級別”的概念。當一個線程 acquire() 了一個鎖後,遞歸等級會設爲 1,此時其他線程無法獲取此鎖,但本線程還可以獲取,使得遞歸等級加 1。本線程釋放一次鎖,就使得遞歸等級減 1。直到減爲 0,此時鎖被釋放。

可重入鎖的方法 acquire()release() 請參看原始鎖,它也支持 “上下文管理協議”。

Condition

Condition 對象就是條件變量,它總是與某種鎖相關聯,可以是外部傳入的鎖或是系統默認創建的鎖。當幾個條件變量共享一個鎖時,你就應該自己傳入一個鎖。這個鎖不需要你操心,Condition 類會管理它。

acquire()release() 可以操控這個相關聯的鎖。其他的方法都必須在這個鎖被鎖上的情況下使用wait() 會釋放這個鎖,阻塞本線程直到其他線程通過 notify()notify_all() 來喚醒它。一旦被喚醒,這個鎖又被 wait() 鎖上。

經典的 consumer/producer 問題的代碼示例爲:

import threading

cv = threading.Condition()

# Consumer
with cv:
    while not an_item_is_available():
        cv.wait()
    get_an_available_item()

# Producer
with cv:
    make_an_item_available()
    cv.notify(

還可以用 wait_for() 來替換 while 循環消費者對系統狀態的判斷:

# Consumer
with cv:
    cv.wait_for(an_item_is_available)
    get_an_available_item()

class Condition(lock=None)

返回一個條件變量。lock 必須是一個 Lock 或 RLock,如果使用默認值 None,系統會自動創建一個 RLock 作爲鎖。這個類的方法簽名列表(用法基本已經介紹清楚了):

  • acquire(*args)
  • release()
  • wait(timeout=None)
  • wait_for(predicate, timeout=None)
  • notify(n=1)
  • notify_all()

Semaphore

這是計算機歷史上最古老的同步原語:信號量。信號量管理着內部的一個計數器,acquire() 使得計數器減 1,release() 使得計數器加 1。當 acquire() 發現計數器爲 0 時,函數會阻塞直到某個線程調用了這個信號量的 release()

class Semaphore(value=1)

它的方法只有 acquire()release(),方法簽名和 Lock 的相同。

class BoundedSemaphore(value=1)

實現有界信號量對象的類。有界信號量是指它的計數器永遠不會超過初始值 valve

Semaphore 例子

信號量通常用於保護容量有限的資源,例如數據庫服務器。在資源大小固定的任何情況下,你應該使用有界信號量。在生成任何工作線程之前,主線程應該初始化信號量:

maxconnections = 5
# ...
pool_sema = BoundedSemaphore(value=maxconnections)

一旦產生,工作線程在需要連接到服務器時調用信號量的獲取和釋放方法(這裏使用了 with 語句):

with pool_sema:
    conn = connectdb()
    try:
        # ... use connection ...
    finally:
        conn.close()

Event

這是線程之間通信的最簡單機制之一:一個線程發出事件信號,其他線程等待它。

class Event

一個事件對象管理着一個標誌,set() 將標誌置爲 Trueclear() 將標誌置爲 Falsewait() 阻塞本線程直到標誌爲 True。最初標誌爲 False。此類的方法簽名列表爲:

  • is_set() -> bool
  • set()
  • clear()
  • wait(timeout=None)

Timer

Timer 表示僅在經過一定時間後才應運行的操作。它是 Thread 的子類。

class Timer(interval, function, args=None, kwargs=None)

方法只有一個 cancel(),用來取消計時器,停止計時器的運行。

例子

def hello():
    print("hello, world")

t = Timer(30.0, hello)
t.start()  # 30 秒後 "hello, world" 會被打印出來

Barrier

此類提供了一個簡單的同步原語,供需要相互等待的固定數量的線程使用。每個線程都試圖通過調用 wait() 方法來跨越屏障,並將阻塞直到所有線程都進行了 wait() 調用。此時,這些線程同時釋放。

class Barrier(parties, action=None, timeout=None)

wait(timeout=None)

返回值爲 parties - 1,每個線程都不相同,可以這樣用:

i = barrier.wait()
if i == 0:
    # 只有一個線程需要打印
    print("passed the barrier")
reset()

將 barrier 重置爲初始狀態。原本等待的線程將會引發 BrokenBarrierError 異常。

abort()

將一個 barrier 設爲破損狀態。當前和之後對這個 barrier 的 wait() 都會引發 BrokenBarrierError。如果一個線程要退出,爲了防止死鎖,應當使用這個方法。

parties

這一組線程的數量。

n_waiting

目前處於等待狀態的線程的數量。

broken

一個布爾值,代表這個 barrier 是否破損。

exception BrokenBarrierError

這個異常是 RuntimeError 的子類,當一個 Barrier 被重置或破損時,它會被引發。

參考

https://docs.python.org/3/library/threading.html#threading.Timer.cancel

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章