一、多線程定義
進程是由若干線程組成的,一個進程至少有一個線程,叫主線程。 多線程類似於同時執行多個不同程序,多線程運行有如下優點:
- 使用線程可以把佔據長時間的程序中的任務放到後臺去處理,不會出現界面卡頓的情況。
- 用戶界面更加友好,這樣比如用戶點擊了一個按鈕去觸發某些事件的處理,可以彈出一個進度條來顯示處理的進度。
- 程序的運行速度可能加快。
- 在一些等待的任務實現上如用戶輸入、文件讀寫和網絡收發數據等,線程就比較有用了。在這種情況下我們可以釋放一些珍貴的資源如內存佔用等等。
線程在執行過程中與進程還是有區別的。每個獨立的進程有一個程序運行的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制,多個線程共享進程的資源,比如內存,文件句柄等。
每個線程都有他自己的一組CPU寄存器,稱爲線程的上下文,該上下文反映了線程上次運行該線程的CPU寄存器的狀態。
指令指針和堆棧指針寄存器是線程上下文中兩個最重要的寄存器,線程總是在進程得到上下文中運行的,這些地址都用於標誌擁有線程的進程地址空間中的內存。
- 線程可以被搶佔(中斷)。
- 在其他線程正在運行時,線程可以暫時擱置(也稱爲睡眠) -- 這就是線程的退讓。
Python的線程雖然是真正的線程,但解釋器執行代碼時,有一個GIL鎖:Global Interpreter Lock,任何Python線程執行前,必須先獲得GIL鎖,然後,每執行100條計步(ticks)(也可以認爲是虛擬機指令或字節碼),解釋器就自動釋放GIL鎖,讓別的線程有機會執行。這個GIL全局鎖實際上把所有線程的執行代碼都給上了鎖,所以,多線程在Python中只能交替執行,即使100個線程跑在100核CPU上,也只能用到1個核。
GIL是Python解釋器設計的歷史遺留問題,通常我們用的解釋器是官方實現的CPython,要真正利用多核,除非重寫一個不帶GIL的解釋器。所以,在Python中,可以使用多線程,但不要指望能有效利用多核。如果一定要通過多線程利用多核,那隻能通過C擴展來實現,不過這樣就失去了Python簡單易用的特點。不過Python雖然不能利用多線程實現多核任務,但可以通過多進程實現多核任務。多個Python進程有各自獨立的GIL鎖,互不影響。
線程的生命週期:
- 新建:使用線程的第一步就是創建線程,創建後的線程只是進入可執行的狀態,也就是Runnable
- Runnable:進入此狀態的線程還並未開始運行,一旦CPU分配時間片給這個線程後,該線程才正式的開始運行
- Running:線程正式開始運行,在運行過程中線程可能會進入阻塞的狀態,即Blocked
- Blocked:在該狀態下,線程暫停運行,解除阻塞後,線程會進入Runnable狀態,等待CPU再次分配時間片給它
- 結束:線程方法執行完畢或者因爲異常終止返回
線程從Running進入Blocked狀態,通常有三種情況:
- 睡眠:線程主動調用sleep()或join()方法後.
- 等待:線程中調用wait()方法,此時需要有其他線程通過notify()方法來喚醒
- 同步:線程中獲取線程鎖,但是因爲資源已經被其他線程佔用時.
二、Python用法
由於線程是操作系統直接支持的執行單元,因此,高級語言通常都內置多線程的支持,Python也不例外,並且,Python的線程是真正的Posix Thread,而不是模擬出來的線程。需要注意的Python多用於處理數據,所以多線程用的地方並不像其他高級語言(如Java,C++)一樣多,因爲GIL的限制,更多的情況下用多進程,多線程更適用於IO操作。
Python3 通過兩個標準庫 _thread (python2中是thread模塊)和 threading 提供對線程的支持。_thread 提供了低級別的、原始的線程以及一個簡單的鎖,它相比於 threading 模塊的功能還是比較有限的。
(1) 設置GIL
(2) 切換到一個線程去運行
(3) 運行(CPU調度過程):
a. 指定數量的字節碼的指令,或者
b. 線程主動讓出控制(可以調用time.sleep(0))
(4) 把線程設置爲睡眠狀態
(5) 解鎖GIL
(6) 再次重複以上所有步驟
驗證GIL:
在一個Python進程中,GIL只有一個。
爲了驗證確實是 GIL 的問題,我們可以用不同的解釋器再執行一次。這裏使用 pypy(有 GIL)和 jython (無 GIL)作測試:
# PyPy, fib
Time elapsed with 1 branch(es): 0.868052 sec(s)
Time elapsed with 2 branch(es): 1.706454 sec(s)
Time elapsed with 3 branch(es): 2.594260 sec(s)
Time elapsed with 4 branch(es): 3.449946 sec(s)
# Jython, fib
Time elapsed with 1 branch(es): 2.984000 sec(s)
Time elapsed with 2 branch(es): 3.058000 sec(s)
Time elapsed with 3 branch(es): 4.404000 sec(s)
Time elapsed with 4 branch(es): 5.357000 sec(s)
從結果可以看出,用 pypy 執行時,時間開銷和線程數也是幾乎成正比的;而 jython 的時間開銷則是以較爲緩慢的速度增長的。jython 由於下面還有一層 JVM,單線程的執行速度很慢,但在線程數達到 4 時,時間開銷只有單線程的兩倍不到,僅僅稍遜於 cpython 的 4 線程運行結果(5.10 secs)。由此可見,GIL 確實是造成僞並行現象的主要因素。爲了解決這個問題,多線程無法實現,需要用到多進程,在其他文章有所介紹。
GIL也有好處:
- 可以增加單線程程序的運行速度(不再需要對所有數據結構分別獲取或釋放鎖)
- 容易和大部分非線程安全的 C 庫進行集成
- 容易實現(使用單獨的 GIL 鎖要比實現無鎖,或者細粒度鎖的解釋器更容易)
2.1 定義
Python中使用線程有兩種方式:函數或者用類來包裝線程對象。
Python的標準庫提供了兩個模塊:thread
和threading
,thread
是低級模塊,threading
是高級模塊,對thread
進行了封裝。絕大多數情況下,我們只需要使用threading
這個高級模塊。
(1)函數式:調用thread模塊中的start_new_thread()函數來產生新線程。
基本語法:
thread.start_new_thread ( function, args[, kwargs] )
參數說明:
- function - 線程函數。
- args - 傳遞給線程函數的參數,必須是個tuple類型。保證不可變類似於Java要求是final。
- kwargs - 可選參數。
start_new_thread()要求一定要有前兩個參數,即使運行的函數不要參數,也要傳一個空的元組。
示例:
# 爲線程定義一個函數
def print_time( threadName, delay):
count = 0
while count < 5:
time.sleep(delay)
count += 1
print "%s: %s" % ( threadName, time.ctime(time.time()) )
# 創建兩個線程
try:
thread.start_new_thread( print_time, ("Thread-1", 2, ) )
thread.start_new_thread( print_time, ("Thread-2", 4, ) )
except:
print "Error: unable to start thread"
while 1:
pass
Thread-1: Thu Jan 22 15:42:17 2009
Thread-1: Thu Jan 22 15:42:19 2009
Thread-2: Thu Jan 22 15:42:19 2009
Thread-1: Thu Jan 22 15:42:21 2009
Thread-2: Thu Jan 22 15:42:23 2009
Thread-1: Thu Jan 22 15:42:23 2009
Thread-1: Thu Jan 22 15:42:25 2009
Thread-2: Thu Jan 22 15:42:27 2009
Thread-2: Thu Jan 22 15:42:31 2009
Thread-2: Thu Jan 22 15:42:35 2009
(2)使用threading模塊創建線程。
1)直接從threading.Thread繼承,然後重寫__init__方法和run方法,用法類似於Java的Thread類:
class myThread (threading.Thread): #繼承父類threading.Thread
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self): #把要執行的代碼寫到run函數裏面 線程在創建後會直接運行run函數
print "Starting " + self.name
print_time(self.name, self.counter, 5)
print "Exiting " + self.name
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
(threading.Thread).exit()
time.sleep(delay)
print "%s: %s" % (threadName, time.ctime(time.time()))
counter -= 1
# 創建新線程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 開啓線程
thread1.start()
thread2.start()
print "Exiting Main Thread"
Starting Thread-1
Starting Thread-2
Exiting Main Thread
Thread-1: Thu Mar 21 09:10:03 2013
Thread-1: Thu Mar 21 09:10:04 2013
Thread-2: Thu Mar 21 09:10:04 2013
Thread-1: Thu Mar 21 09:10:05 2013
Thread-1: Thu Mar 21 09:10:06 2013
Thread-2: Thu Mar 21 09:10:06 2013
Thread-1: Thu Mar 21 09:10:07 2013
Exiting Thread-1
Thread-2: Thu Mar 21 09:10:08 2013
Thread-2: Thu Mar 21 09:10:10 2013
Thread-2: Thu Mar 21 09:10:12 2013
Exiting Thread-2
最常用:2)繼承一個類,對Java來說很正常也很常用,但是對於Python來說有點浪費,Python更喜歡函數式編程。所以另一種方式是啓動一個線程就是把一個函數傳入並創建Thread
實例,然後調用start()
開始執行:
實例化一個Thread調用Thread()方法與調用thread.start_new_thread()之間的最大區別是:新的線程不會立即開始。在你創建線程對象,但不想馬上開始運行線程的時候,這是一個很有用的同步特性。
實際上首先Thread類會檢測傳入的target是否是None,如果是則執行內部的run()方法;如果不是None,說明傳入了一個目標執行函數target,執行target即可。
args: 線程執行方法接收的參數,該屬性是一個元組,如果只有一個參數也需要在末尾加逗號。
# 新線程執行的代碼:
def loop():
print 'thread %s is running...' % threading.current_thread().name
n = 0
while n < 5:
n = n + 1
print 'thread %s >>> %s' % (threading.current_thread().name, n)
time.sleep(1)
print 'thread %s ended.' % threading.current_thread().name
print 'thread %s is running...' % threading.current_thread().name
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join() # 爲了線程執行結束打印主線程名。等待調用線程執行結束。
print 'thread %s ended.' % threading.current_thread().name
thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.
3) 創建一個Thread實例,傳給它一個可調用的類對象
這是第二個方法,與傳一個函數很相似,但它是傳一個可調用的類的實例供線程啓動的時候執行,這是多線程編程的一個更爲面向對象的方法。相對於一個或幾個函數來說,類對象可以保存更多的信息,這種方法更爲靈活。
許多的python 對象都是我們所說的可調用的,即是任何能通過函數操作符“()”來調用的對象(見《python核心編程》第14章)。類的對象也是可以調用的,當被調用時會自動調用對象的內建方法__call__(),因此這種新建線程的方法就是給線程指定一個__call__方法被重載了的對象。
loops = [4,2] #睡眠時間
class ThreadFunc(object):
def __init__(self, func, args, name=''):
self.name=name
self.func=func
self.args=args
def __call__(self): # 實際的函數執行體,執行邏輯
apply(self.func, self.args)
def loop(nloop, nsec):
print "Start loop", nloop, 'at:', ctime()
sleep(nsec)
print 'Loop', nloop, 'done at:', ctime()
def main():
print 'Starting at:', ctime()
threads=[]
nloops = range(len(loops)) #列表[0,1]
for i in nloops:
#調用ThreadFunc類實例化的對象,創建所有線程
t = threading.Thread(
target=ThreadFunc(loop, (i,loops[i]), loop.__name__)
)
threads.append(t)
#開始線程
for i in nloops:
threads[i].start()
#等待所有結束線程
for i in nloops:
threads[i].join()
print 'All end:', ctime()
通過with語句使用線程鎖
所有的線程鎖都有一個加鎖和釋放鎖的動作,非常類似文件的打開和關閉。在加鎖後,如果線程執行過程中出現異常或者錯誤,沒有正常的釋放鎖,那麼其他的線程會造到致命性的影響。通過with上下文管理器,可以確保鎖被正常釋放。其格式如下:
with some_lock:
# 執行任務...
這相當於:
some_lock.acquire()
try:
# 執行任務..
finally:
some_lock.release()
2.2 線程模塊
Python通過兩個標準庫thread和threading提供對線程的支持。
2.2.1 thread模塊
thread提供了低級別的、原始的線程以及一個簡單的鎖。
2.2.2 threading模塊
由於任何進程默認就會啓動一個線程,我們把該線程稱爲主線程,主線程又可以啓動新的線程,Python的threading
模塊有個current_thread()
函數,它永遠返回當前線程的實例。主線程實例的名字叫MainThread
,子線程的名字在創建時指定。名字僅僅在打印時用來顯示,完全沒有其他意義,如果不起名字Python就自動給線程命名爲Thread-1
,Thread-2
……。
thread模塊不支持守護線程。當主線程退出時,所有的子線程不論它們是否還在工作,都會被強行退出。這就引入了守護線程的概念。Threading模塊支持守護線程,它們工作流程如下:
守護線程一般是一個等待客戶請求的服務器,如果沒有客戶提出請求,它就在那等着。如果你設定一個線程爲守護線程,就表示你在說這個線程是不重要的,在進程退出時,不用等待這個線程退出,正如網絡編程中服務器線程運行在一個無限循環中,一般不會退出的。
如果你的主線程要退出的時候,不用等待那些子線程完成,那就設定這些線程的daemon屬性。即,線程開始(調用thread.start())之前,調用setDaemon()函數設定線程的daemon標準(thread.setDaemon(True))就表示這個線程“不重要”。如果你想要等待子線程完成再退出,那就什麼都不用做,或者顯示地調用thread.setDaemon(False)以保證其daemon標誌位False。你可以調用thread.isDaemon()函數來判斷其daemon標誌的值。
新的子線程會繼承其父線程的daemon標誌,整個Python會在所有的非守護線程退出後纔會結束,即進程中沒有非守護線程存在的時候才結束。
thread模塊基本被廢棄了,現在多用threading模塊來創建和管理子線程,有兩種方式來創建線程:一種是繼承Thread類,並重寫它的run()方法;另一種是在實例化threading.Thread
對象的時候,將線程要執行的任務函數作爲參數傳入線程。
Thread類聲明如下:
threading.Thread(self, group=None, target=None, name=None,
args=(), kwargs=None, *, daemon=None)
- 參數group是預留的,用於將來擴展;
- 參數target是一個可調用對象,在線程啓動後執行;
- 參數name是線程的名字。默認值爲“Thread-N“,N是一個數字。
- 參數args和kwargs分別表示調用target時的參數列表和關鍵字參數。
threading 模塊提供的其他方法:
- threading.currentThread(): 返回當前的線程變量。
- threading.enumerate(): 返回一個包含正在運行的線程的list。正在運行指線程啓動後、結束前,不包括啓動前和終止後的線程。
- threading.activeCount(): 返回正在運行的線程數量,與len(threading.enumerate())有相同的結果。
除了使用方法外,線程模塊同樣提供了Thread類來處理線程,Thread類提供了以下方法:
- run(): 表示線程活動的方法,線程被cpu調度後自動執行的方法。
- start():啓動線程活動,等待CPU調度。
- join([time]): 阻塞進程直到線程執行完畢,進程(也就是所有線程)等待至其他線程終止。這阻塞調用線程直至線程的join() 方法被調用終止-正常退出或者拋出未處理的異常-或者是可選的超時發生。.阻塞主線程(擋住,無法執行join以後的語句),專注執行多線程。多線程多join的情況下,依次執行各線程的join方法,前頭一個結束了才能執行後面一個。無參數,則等待到該線程結束,纔開始執行下一個線程的join。
調用Thread.join將會使主調線程堵塞,直到被調用線程運行結束或超時。參數timeout是一個數值類型,表示超時時間,如果未提供該參數,那麼主調線程將一直堵塞到被調線程結束
- isAlive(): 返回線程是否活動的。
- getName(): 返回線程名。
- setName(): 設置線程名。
2.3 線程結束問題
(1)線程的結束一般依靠線程函數的自然結束,一般不是循環操作;
(2)可以線程中任務結束後,在線程函數中調用thread.exit(),他拋出SystemExit exception,達到退出線程的目的;
(3)利用一個全局變量flag來控制是否在線程函數中調用thread.exit()。
(4)實際上在真正的編程中大部分情況下可能是循環操作,比如網絡編程中循環監聽端口。一般用一個全局變量flag控制循環執行,函數外設置flag爲false結束線程。
當一個線程結束計算,它就退出了。線程可以調用thread.exit()之類的退出函數,也可以使用Python退出進程的標準方法,如sys.exit()或拋出一個SystemExit異常等。不過,不可以直接殺掉Kill一個線程。
2.4 線程同步
多線程開發中最難的問題不是如何使用,而是如何寫出正確高效的代碼,要寫出正確而高效的代碼必須要理解兩個很重要的概念:同步和通信。
所謂的通信指的是線程之間如何交換消息,而同步則用於控制不同線程之間操作發生的相對順序。簡單點說同步就是控制多個線程訪問代碼的順序,通信就是線程之間如何傳遞消息。在python中實現同步的最簡單的方案就是使用鎖機制,實現通信最簡單的方案就是Event。
2.4.1 threading模塊join函數
示例:A 線程正在運行,當B線程進行Join操作後,A線程會被阻斷,進入等待隊列。B線程執行,當B線程執行完畢後,B線程的資源收回,A線程進去執行隊列。A線程繼續進行執行。
總結:也就是調用的線程先執行,其他線程等待,執行結束後再執行其他線程。
1、python 默認參數創建線程後,不管主線程是否執行完畢,都會等待子線程執行完畢才一起退出。
2、如果創建線程,並且設置了daemon爲true,即thread.setDaemon(True), 則主線程執行完畢後自動退出,不會等待子線程的執行結果。而且隨着主線程退出,子線程也消亡。設置子線程爲守護線程。默認是False,非守護線程。
3、join方法的作用是阻塞,等待子線程結束,join方法有一個參數是timeout,即如果主線程等待timeout,子線程還沒有結束,則主線程強制結束子線程。
4、如果線程daemon屬性爲False, 則join裏的timeout參數無效。主線程會一直等待子線程結束。
5、如果線程daemon屬性爲True, 則join裏的timeout參數是有效的, 主線程會等待timeout時間後,結束子線程。此處有一個坑,即如果同時有N個子線程join(timeout),那麼實際上主線程會等待的超時時間最長爲 N * timeout, 因爲每個子線程的超時開始時刻是上一個子線程超時結束的時刻。
在主線程A中調用了B.setDaemon(),這個的意思是,把子線程B設置爲主線程A的守護線程,這時候,要是主線程A執行結束了,就不管子線程B是否完成,一併和主線程A退出。這就是setDaemon方法的含義,這基本和join是相反的。此外,還有個要特別注意的:必須在start() 方法調用之前設置,如果不設置爲守護線程,程序會被無限掛起。
顧名思義是守護主線程的子線程,所以主線程執行完畢,守護線程也沒必要繼續執行,直接退出即可。
示例:
def doThreadTest():
print 'start thread time:', time.strftime('%H:%M:%S')
time.sleep(10)
print 'stop thread time:', time.strftime('%H:%M:%S')
threads = []
for i in range(3):
thread1 = threading.Thread(target=doThreadTest)
thread1.setDaemon(True)
threads.append(thread1)
for t in threads:
t.start()
for t in threads:
t.join(1)
print 'stop main thread'
start thread time: 19:31:15
start thread time: 19:31:15
start thread time: 19:31:15
stop main thread
如果把thread1.setDaemon(True) 註釋掉, 運行結果爲:
start thread time: 19:32:30
start thread time: 19:32:30
start thread time: 19:32:30
stop main thread
stop thread time: 19:32:40
stop thread time: 19:32:40
stop thread time: 19:32:40
如果去掉join的參數:
start thread time: 19:34:21
start thread time: 19:34:21
start thread time: 19:34:21
stop thread time: 19:34:26
stop thread time: 19:34:26
stop thread time: 19:34:26
stop main thread
爲了防止髒數據而使用join()的方法,其實是讓多線程變成了單線程,屬於因噎廢食的做法,正確的做法是使用線程鎖。Python在threading模塊中定義了幾種線程鎖類。
2.4.2 線程鎖
threading模塊所提供的鎖類型:
- Lock:互斥鎖
- RLock:可重入鎖,使單一進程再次獲得已持有的鎖(遞歸鎖)
- Condition:條件鎖,使得一個線程等待另一個線程滿足特定條件,比如改變狀態或某個值。
- Semaphore:信號鎖。爲線程間共享的有限資源提供一個”計數器”,如果沒有可用資源則會被阻塞。
- Event:事件鎖,任意數量的線程等待某個事件的發生,在該事件發生後所有線程被激活
- Timer:一種計時器
- Barrier:Python3.2新增的“阻礙”類,必須達到指定數量的線程後纔可以繼續執行。
如果多個線程共同對某個數據修改,則可能出現不可預料的結果,爲了保證數據的正確性,需要對多個線程進行同步。通常鎖還與try/finally及with一起用,爲了保證鎖一定會被釋放,如果發生exception,鎖不釋放就會出現問題。也叫原語鎖、簡單鎖、互斥鎖、互斥量、二值信號量。
多線程和多進程最大的不同在於,多進程中,同一個變量,各自有一份拷貝存在於每個進程中,互不影響,而多線程中,所有變量都由所有線程共享所在進程的資源和數據,所以,任何一個變量都可以被任何一個線程修改,因此,線程之間共享數據最大的危險在於多個線程同時改一個變量,把內容給改亂了。
Condition是在Lock/RLock的基礎上再次包裝而成,而Semaphore的原理和操作系統的PV操作一致。
使用Thread對象的Lock和Rlock可以實現簡單的線程同步,這兩個對象都有acquire方法(請求鎖)和release(釋放鎖)方法,對於那些需要每次只允許一個線程操作的數據,可以將其操作放到acquire和release方法之間。如下:
class myThread (threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print "Starting " + self.name
# 獲得鎖,成功獲得鎖定後返回True
# 可選的timeout參數不填時將一直阻塞直到獲得鎖定
# 否則超時後將返回False
threadLock.acquire()
print_time(self.name, self.counter, 3)
# 釋放鎖
threadLock.release()
def print_time(threadName, delay, counter):
while counter:
time.sleep(delay)
print "%s: %s" % (threadName, time.ctime(time.time()))
counter -= 1
threadLock = threading.Lock()
threads = []
# 創建新線程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 開啓新線程
thread1.start()
thread2.start()
# 添加線程到線程列表
threads.append(thread1)
threads.append(thread2)
# 等待所有線程完成
for t in threads:
t.join()
print "Exiting Main Thread"
多線程的優勢在於可以同時運行多個任務(至少感覺起來是這樣,實際不是,是併發,不是並行)。但是當線程需要共享數據時,可能存在數據不同步的問題。爲了避免這種情況,引入了鎖的概念。
鎖有兩種狀態——鎖定和未鎖定。每當一個線程比如"set"要訪問共享數據時,必須先獲得鎖定;如果已經有別的線程比如"print"獲得鎖定了,那麼就讓線程"set"暫停,也就是同步阻塞;等到線程"print"訪問完畢,釋放鎖以後,再讓線程"set"繼續。
經過這樣的處理,打印列表時要麼全部輸出0,要麼全部輸出1,不會再出現一半0一半1的尷尬場面。類似於Java的synchronizedde用法。
Lock (互斥鎖Mutex)。互斥鎖是一種獨佔鎖,同一時刻只有一個線程可以訪問共享的數據。使用很簡單,初始化鎖對象,然後將鎖當做參數傳遞給任務函數或者利用全局對象傳遞參數,在任務中加鎖,使用後釋放鎖。
重入鎖(Re-Entrant Lock)。 可重入鎖對象。使單線程可以再次獲得已經獲得了的鎖(遞歸鎖定)。RLock允許在同一線程中被多次acquire。而Lock卻不允許這種情況。否則會出現死循環,程序不知道解哪一把鎖。注意:如果使用RLock,那麼acquire和release必須成對出現,即調用了n次acquire,必須調用n次的release才能真正釋放所佔用的鎖。
RLock的使用方法和Lock一模一樣,只不過它支持重入鎖。該鎖對象內部維護着一個Lock和一個counter對象。counter對象記錄了acquire的次數,使得資源可以被多次require。最後,當所有RLock被release後,其他線程才能獲取資源。在同一個線程中,RLock.acquire()
可以被多次調用,利用該特性,可以解決部分死鎖問題。
也就是同一個線程可以獲得多個鎖,利用計數器來實現,然後逐步清除即可。鎖沒有釋放,也可以繼續請求鎖,不會阻塞。但是RLock是線程級別的,在哪個線程acquire的,就需要在這個線程release,其它線程無法release。也就是說RLock無法跨線程。需要跨線程就得使用Lock。從而避免引起髒數據問題。
2.4.3 信號量
Threading 模塊包含對其他功能的支持。例如,可以創建信號量(Semaphore),這是計算機科學中最古老的同步原語之一。基本上,一個信號量管理一個內置的計數器。當你調用 acquire 時計數器就會遞減,相反當你調用 release 時就會遞增。根據其設計,計數器的值無法小於零,所以如果正好在計數器爲零時調用 acquire 方法,該方法將阻塞線程。通常使用信號量時都會初始化一個大於零的值,如 semaphore = threading.Semaphore(2)。
示例:
def run(n, se):
se.acquire()
print("run the thread: %s" % n)
time.sleep(1)
se.release()
# 設置允許5個線程同時運行
semaphore = threading.BoundedSemaphore(5)
for i in range(20):
t = threading.Thread(target=run, args=(i,semaphore))
t.start()
運行後,可以看到5個一批的線程被放行。
mutex 互斥鎖 是semaphore的一種特殊情況(n=1時)。也就是說,完全可以用後者替代前者。但是,因爲mutex較爲簡單,且效率高,所以在必須保證資源獨佔的情況下,還是採用這種設計。
2.4.4 Event事件
另一個非常有用的同步工具就是事件(Event)。它允許你使用信號(signal)實現線程通信。Python提供了Event對象用於線程間通信,它是由線程設置的信號標誌,如果信號標誌爲真,則其他線程等待直到信號清除。
Event對象實現了簡單的線程通信機制,它提供了設置信號,清除信號,等待等用於實現線程間的通信。
event = threading.Event() 創建一個event
事件線程鎖的運行機制:全局定義了一個Flag,如果Flag的值爲False,那麼當程序執行wait()方法時就會阻塞,如果Flag值爲True,線程不再阻塞。這種鎖,類似交通紅綠燈(默認是紅燈),它屬於在紅燈的時候一次性阻擋所有線程,在綠燈的時候,一次性放行所有排隊中的線程。
1 設置信號
event.set()
使用Event的se()t方法可以設置Event對象內部的信號標誌爲真。Event對象提供了isSet()方法來判斷其內部信號標誌的狀態。
當使用event對象的set()方法後,isSet()方法返回真
2 清除信號
event.clear()
使用Event對象的clear()方法可以清除Event對象內部的信號標誌,即將其設爲假,當使用Event的clear方法後,isSet()方法返回假。
3 等待
event.wait()
Event對象wait的方法只有在內部信號爲真的時候纔會很快的執行並完成返回。當Event對象的內部信號標誌爲假時,
則wait方法一直等待到其爲真時才返回。也就是說必須set新號標誌位真。
示例:
def do(event):
print('start')
event.wait()
print('execute')
event_obj = threading.Event()
for i in range(10):
t = threading.Thread(target=do, args=(event_obj,))
t.start()
event_obj.clear()
inp = input('輸入內容:')
if inp == 'true':
event_obj.set()
start
start
start
start
start
start
start
start
start
start
輸入內容:'true'
execute
execute
execute
execute
execute
execute
execute
execute
execute
execute
2.4.5 條件Condition
Condition稱作條件鎖,依然是通過acquire()/release()加鎖解鎖。
wait([timeout])方法將使線程進入Condition的等待池等待通知,並釋放鎖。使用前線程必須已獲得鎖定,否則將拋出異常。
notify()方法將從等待池挑選一個線程並通知,收到通知的線程將自動調用acquire()嘗試獲得鎖定(進入鎖定池),其他線程仍然在等待池中。調用這個方法不會釋放鎖定。使用前線程必須已獲得鎖定,否則將拋出異常。
notifyAll()方法將通知等待池中所有的線程,這些線程都將進入鎖定池嘗試獲得鎖定。調用這個方法不會釋放鎖定。使用前線程必須已獲得鎖定,否則將拋出異常。
2.4.6 Barrier
最後,在 Python 3.2 中加入了 Barrier 對象。Barrier 是管理線程池中的同步原語,在線程池中多條線程需要相互等待對方。如果要傳遞 barrier,每一條線程都要調用 wait() 方法,在其他線程調用該方法之前線程將會阻塞。全部調用之後將會同時釋放所有線程。
2.4.7 定時器Timer
定時器Timer類是threading模塊中的一個小工具,用於指定n秒後執行某操作。
from threading import Timer
def hello():
print("hello, world")
# 表示1秒後執行hello函數
t = Timer(1, hello)
t.start()
2.4.8 創建線程本地數據
我們知道進程之間數據互不干擾,而進程內的線程共享進程數據、資源和地址空間等。有些場景下,我們希望每個線程,都有自己獨立的數據,他們使用同一個變量,但是在每個線程內的數據都是獨立的互不干擾的,類似於進程中的數據。
我們可以使用threading.local() 相當於創建線程局部變量來實現:
import threading
L = threading.local()
L.num = 1
# 此時操作的是我們當前主線程的threading.local()對象,輸出結果爲1
print(L.num)
def f():
print(L.num)
# 創建一個子線程,去調用f(),看能否訪問主線程中定義的L.num
t = threading.Thread(target=f)
t.start()
# 結果提示我們:
# AttributeError: '_thread._local' object has no attribute 'num'
對上面的稍作修改:
import threading
L = threading.local()
L.num = 1
# 此時操作的是我們當前主線程的threading.local()對象,輸出結果爲1
print(L.num)
def f():
L.num = 5 # 重新賦值,相當於新定義的變量
# 這裏可以成功的輸出5
print(L.num)
# 創建一個子線程,去調用f(),看能否訪問主線程中定義的L.num
t = threading.Thread(target=f)
t.start()
# 主線程中的L.num依然是1,沒有發生任何改變
print(L.num)
程序運行結果爲:
1
5
1
由此可見,threading.local()創建的對象中的屬性,是對於每個線程獨立存在的,它們相互之間無法干擾,我們稱它爲線程本地數據。
全局變量L
就是一個ThreadLocal
對象,每個Thread
對它都可以讀寫student
屬性,但互不影響。你可以把L
看成全局變量,但每個屬性如L.num
都是線程的局部變量,可以任意讀寫而互不干擾,也不用管理鎖的問題,ThreadLocal
內部會處理。
可以理解爲全局變量L
是一個dict
,不但可以用L.num
,還可以綁定其他變量,如L.num1
等等。
ThreadLocal
最常用的地方就是爲每個線程綁定一個數據庫連接,HTTP請求,用戶身份信息等,這樣一個線程的所有調用到的處理函數都可以非常方便地訪問這些資源。
2.5 線程通信
2.5.1 線程優先級隊列( Queue)
Python的Queue模塊中提供了同步的、線程安全的隊列類,包括FIFO(先入先出)隊列Queue,LIFO(後入先出)隊列LifoQueue,和優先級隊列PriorityQueue。這些隊列都實現了鎖原語,能夠在多線程中直接使用。可以使用隊列來實現線程間的同步。用於信息和資源共享。
凡是符合該種結構的多線程通信過程我們稱之爲生產者-消費者模型(應屆生面試常問)。
Queue模塊中的常用方法:
- Queue.qsize() 返回隊列的大小
- Queue.empty() 如果隊列爲空,返回True,反之False
- Queue.full() 如果隊列滿了,返回True,反之False
- Queue.full 與 maxsize 大小對應
- Queue.get([block[, timeout]])獲取隊列,timeout等待時間
- Queue.get_nowait() 相當Queue.get(False)
- Queue.put(item) 寫入隊列,timeout等待時間
- Queue.put_nowait(item) 相當Queue.put(item, False)
- Queue.task_done() 在完成一項工作之後,Queue.task_done()函數向任務已經完成的隊列發送一個信號
- Queue.join() 實際上意味着等到隊列爲空,再執行別的操作
exitFlag = 0
class myThread (threading.Thread):
def __init__(self, threadID, name, q):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.q = q
def run(self):
print "Starting " + self.name
process_data(self.name, self.q)
print "Exiting " + self.name
def process_data(threadName, q):
while not exitFlag:
queueLock.acquire()
if not workQueue.empty():
data = q.get()
queueLock.release()
print "%s processing %s" % (threadName, data)
else:
queueLock.release()
time.sleep(1)
threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = Queue.Queue(10)
threads = []
threadID = 1
# 創建新線程
for tName in threadList:
thread = myThread(threadID, tName, workQueue)
thread.start()
threads.append(thread)
threadID += 1
# 填充隊列
queueLock.acquire()
for word in nameList:
workQueue.put(word)
queueLock.release()
# 等待隊列清空
while not workQueue.empty():
pass
# 通知線程是時候退出
exitFlag = 1
# 等待所有線程完成
for t in threads:
t.join()
print "Exiting Main Thread"
Starting Thread-1
Starting Thread-2
Starting Thread-3
Thread-1 processing One
Thread-2 processing Two
Thread-3 processing Three
Thread-1 processing Four
Thread-2 processing Five
Exiting Thread-3
Exiting Thread-1
Exiting Thread-2
Exiting Main Thread
其實Event等更像是線程通信。
三、總結
(1)thread模塊創建多線程有一種方法。threading模塊創建多線程有兩種方法:繼承Thread類和直接利用Thread類。
(2)鎖的好處就是確保了某段關鍵代碼只能由一個線程從頭到尾完整地執行,壞處當然也很多,首先是阻止了多線程併發執行,包含鎖的某段代碼實際上只能以單線程模式執行,效率就大大地下降了。其次,由於可以存在多個鎖,不同的線程持有不同的鎖,並試圖獲取對方持有的鎖時,可能會造成死鎖,導致多個線程全部掛起,既不能執行,也無法結束,只能靠操作系統強制終止。
(3)默認子線程非守護線程,也就是各個子線程和主線程同時執行,各自執行互不干擾。最後等待子線程執行完成,主線程才退出(此時主線程有可能任務執行完,但是沒有退出)。而join函數的不同點是,在運行join函數的地方主線程阻塞,後面的任務不執行,等待子線程執行完成之後,主線程繼續執行然後退出(如果所有子線程都執行完畢,否則在主線程執行完任務之後還需要等待其他子線程執行完畢後才能退出)。
(4)鎖可以去with等語句共用,比如 with lock,即可實現鎖的請求與釋放。
(5)當我們需要編寫併發爬蟲等IO密集型的程序時,應該選用多線程或者協程(親測差距不是特別明顯);當我們需要科學計算,設計CPU密集型程序,應該選用多進程。當然以上結論的前提是,不做分佈式,只在一臺服務器上測試。