引言&動機
考慮一下這個場景,我們有10000條資料需要處理,處理每條資料需要花費1秒,但讀取資料只需要0.1秒,每條資料互不干擾。該如何執行才能花費時間最短呢?
在多執行緒(MT)程式設計出現之前,電腦程式的執行由一個執行序列組成,執行序列按順序在主機的中央處理器(CPU)中執行。無論是任務本身要求順序執行還是整個程式是由多個子任務組成,程式都是按這種方式執行的。即使子任務相互獨立,互相無關(即,一個子任務的結果不影響其它子 任務的結果)時也是這樣。
對於上邊的問題,如果使用一個執行序列來完成,我們大約需要花費 10000*0.1 + 10000 = 11000 秒。這個時間顯然是太長了。
那我們有沒有可能在執行計算的同時取資料呢?或者是同時處理幾條資料呢?如果可以,這樣就能大幅提高任務的效率。這就是多執行緒程式設計的目的。
對於本質上就是非同步的, 需要有多個併發事務,各個事務的執行順序可以是不確定的,隨機的,不可預測的問題,多執行緒是最理想的解決方案。這樣的任務可以被分成多個執行流,每個流都有一個要完成的目標,然後將得到的結果合併,得到最終的結果。
執行緒和程式
什麼是程式
程式(有時被稱為重量級程式)是程式的一次 執行。每個程式都有自己的地址空間,記憶體,資料棧以及其它記錄其執行軌跡的輔助資料。操作系 統管理在其上執行的所有程式,併為這些程式公平地分配時間。程式也可以通過 fork 和 spawn 操作 來完成其它的任務。不過各個程式有自己的記憶體空間,資料棧等,所以只能使用程式間通訊(IPC), 而不能直接共享資訊。
什麼是執行緒
執行緒(有時被稱為輕量級程式)跟程式有些相似,不同的是,所有的執行緒執行在同一個程式中, 共享相同的執行環境。它們可以想像成是在主程式或“主執行緒”中並行執行的“迷你程式”。
執行緒狀態如圖
執行緒有開始,順序執行和結束三部分。它有一個自己的指令指標,記錄自己執行到什麼地方。 執行緒的執行可能被搶佔(中斷),或暫時的被掛起(也叫睡眠),讓其它的執行緒執行,這叫做讓步。 一個程式中的各個執行緒之間共享同一片資料空間,所以執行緒之間可以比程式之間更方便地共享資料以及相互通訊。
當然,這樣的共享並不是完全沒有危險的。如果多個執行緒共同訪問同一片資料,則由於資料訪 問的順序不一樣,有可能導致資料結果的不一致的問題。這叫做競態條件(race condition)。
執行緒一般都是併發執行的,不過在單 CPU 的系統中,真正的併發是不可能的,每個執行緒會被安排成每次只執行一小會,然後就把 CPU 讓出來,讓其它的執行緒去執行。由於有的函式會在完成之前阻塞住,在沒有特別為多執行緒做修改的情 況下,這種“貪婪”的函式會讓 CPU 的時間分配有所傾斜。導致各個執行緒分配到的執行時間可能不 盡相同,不盡公平。
Python、執行緒和全域性直譯器鎖
全域性直譯器鎖(GIL)
首先需要明確的一點是GIL並不是Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。就好比C++是一套語言(語法)標準,但是可以用不同的編譯器來編譯成可執行程式碼。同樣一段程式碼可以通過CPython,PyPy,Psyco等不同的Python執行環境來執行(其中的JPython就沒有GIL)。
那麼CPython實現中的GIL又是什麼呢?GIL全稱Global Interpreter Lock為了避免誤導,我們還是來看一下官方給出的解釋:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
儘管Python完全支援多執行緒程式設計, 但是直譯器的C語言實現部分在完全並行執行時並不是執行緒安全的。 實際上,直譯器被一個全域性直譯器鎖保護著,它確保任何時候都只有一個Python執行緒執行。
在多執行緒環境中,Python 虛擬機器按以下方式執行:
- 設定GIL
- 切換到一個執行緒去執行
- 執行
- 指定數量的位元組碼指令
- 執行緒主動讓出控制(可以呼叫time.sleep(0))
- 把執行緒設定完睡眠狀態
- 解鎖GIL
- 再次重複以上步驟
對所有面向 I/O 的(會呼叫內建的作業系統 C 程式碼的)程式來說,GIL 會在這個 I/O 呼叫之 前被釋放,以允許其它的執行緒在這個執行緒等待 I/O 的時候執行。如果某執行緒並未使用很多 I/O 操作, 它會在自己的時間片內一直佔用處理器(和 GIL)。也就是說,I/O 密集型的 Python 程式比計算密集 型的程式更能充分利用多執行緒環境的好處。
退出執行緒
當一個執行緒結束計算,它就退出了。執行緒可以呼叫 thread.exit()之類的退出函式,也可以使用 Python 退出程式的標準方法,如 sys.exit()或丟擲一個 SystemExit 異常等。不過,你不可以直接 “殺掉”(“kill”)一個執行緒。
在 Python 中使用執行緒
在 Win32 和 Linux, Solaris, MacOS, *BSD 等大多數類 Unix 系統上執行時,Python 支援多執行緒 程式設計。Python 使用 POSIX 相容的執行緒,即 pthreads。
預設情況下,只要在直譯器中
1 |
>> import thread |
如果沒有報錯,則說明執行緒可用。
Python 的 threading 模組
Python 供了幾個用於多執行緒程式設計的模組,包括 thread, threading 和 Queue 等。thread 和 threading 模組允許程式設計師建立和管理執行緒。thread 模組 供了基本的執行緒和鎖的支援,而 threading 供了更高階別,功能更強的執行緒管理的功能。Queue 模組允許使用者建立一個可以用於多個執行緒之間 共享資料的佇列資料結構。
核心 示:避免使用 thread 模組
出於以下幾點考慮,我們不建議您使用 thread 模組。
- 更高階別的 threading 模組更為先 進,對執行緒的支援更為完善,而且使用 thread 模組裡的屬性有可能會與 threading 出現衝突。其次, 低階別的 thread 模組的同步原語很少(實際上只有一個),而 threading 模組則有很多。
- 對於你的程式什麼時候應該結束完全沒有控制,當主執行緒結束 時,所有的執行緒都會被強制結束掉,沒有警告也不會有正常的清除工作。我們之前說過,至少 threading 模組能確保重要的子執行緒退出後程式才退出。
thread 模組
除了產生執行緒外,thread 模組也提供了基本的同步數 據結構鎖物件(lock object,也叫原語鎖,簡單鎖,互斥鎖,互斥量,二值訊號量)。
thread 模組函式
- start_new_thread(function, args, kwargs=None):產生一個新的執行緒,在新執行緒中用指定的引數和可選的 kwargs 來呼叫這個函式。
- allocate_lock():分配一個 LockType 型別的鎖物件
- exit():讓執行緒退出
- acquire(wait=None):嘗試獲取鎖物件
- locked():如果獲取了鎖物件返回 True,否則返回 False
- release():釋放鎖
下面是一個使用 thread 的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import thread from time import sleep, time def loop(num): print('start loop at:', time()) sleep(num) print('loop done at:', time()) def loop1(num): print('start loop 1 at:', time()) sleep(num) print('loop 1 done at:', time()) def main(): print('starting at:', time()) thread.start_new_thread(loop, (4,)) thread.start_new_thread(loop1, (5,)) sleep(6) print('all DONE at:', time()) if __name__ == '__main__': main() ('starting at:', 1489387024.886667) ('start loop at:', 1489387024.88705) ('start loop 1 at:', 1489387024.887277) ('loop done at:', 1489387028.888182) ('loop 1 done at:', 1489387029.888904) ('all DONE at:', 1489387030.889918) |
start_new_thread()要求一定要有前兩個引數。所以,就算我們想要執行的函式不要引數,也要傳一個空的元組。
為什麼要加上sleep(6)這一句呢? 因為,如果我們沒有讓主執行緒停下來,那主執行緒就會執行下一條語句,顯示 “all done”,然後就關閉執行著 loop()和 loop1()的兩個執行緒,退出了。
我們有沒有更好的辦法替換使用sleep() 這種不靠譜的同步方式呢?答案是使用鎖,使用了鎖,我們就可以在兩個執行緒都退出之後馬上退出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#! -*- coding: utf-8 -*- import thread from time import sleep, time loops = [4, 2] def loop(nloop, nsec, lock): print('start loop %s at: %s' % (nloop, time())) sleep(nsec) print('loop %s done at: %s' % (nloop, time())) # 每個執行緒都會被分配一個事先已經獲得的鎖,在 sleep()的時間到了之後就釋放 相應的鎖以通知主執行緒,這個執行緒已經結束了。 lock.release() def main(): print('starting at:', time()) locks = [] nloops = range(len(loops)) for i in nloops: # 呼叫 thread.allocate_lock()函式建立一個鎖的列表 lock = thread.allocate_lock() # 分別呼叫各個鎖的 acquire()函式獲得, 獲得鎖表示“把鎖鎖上” lock.acquire() locks.append(lock) for i in nloops: # 建立執行緒,每個執行緒都用各自的迴圈號,睡眠時間和鎖為引數去呼叫 loop()函式 thread.start_new_thread(loop, (i, loops[i], locks[i])) for i in nloops: # 線上程結束的時候,執行緒要自己去做解鎖操作 # 當前迴圈只是坐在那一直等(達到暫停主 執行緒的目的),直到兩個鎖都被解鎖為止才繼續執行。 while locks[i].locked(): pass print('all DONE at:', time()) if __name__ == '__main__': main() |
為什麼我們不在建立鎖的迴圈裡建立執行緒呢?有以下幾個原因:
- 我們想到實現執行緒的同步,所以要讓“所有的馬同時衝出柵欄”。
- 獲取鎖要花一些時間,如果你的 執行緒退出得“太快”,可能會導致還沒有獲得鎖,執行緒就已經結束了的情況。
threading 模組
threading 模組不僅提供了 Thread 類,還 供了各 種非常好用的同步機制。
下面是threading 模組裡所有的物件:
- Thread: 表示一個執行緒的執行的物件
- Lock: 鎖原語物件(跟 thread 模組裡的鎖物件相同)
- RLock: 可重入鎖物件。使單執行緒可以再次獲得已經獲得了的鎖(遞迴鎖定)。
- Condition: 條件變數物件能讓一個執行緒停下來,等待其它執行緒滿足了某個“條件”。 如,狀態的改變或值的改變。
- Event: 通用的條件變數。多個執行緒可以等待某個事件的發生,在事件發生後, 所有的執行緒都會被啟用。
- Semaphore: 為等待鎖的執行緒 供一個類似“等候室”的結構
- BoundedSemaphore: 與 Semaphore 類似,只是它不允許超過初始值
- Timer: 與 Thread 相似,只是,它要等待一段時間後才開始執行。
守護執行緒
另一個避免使用 thread 模組的原因是,它不支援守護執行緒。當主執行緒退出時,所有的子執行緒不 論它們是否還在工作,都會被強行退出。有時,我們並不期望這種行為,這時,就引入了守護執行緒 的概念
threading 模組支援守護執行緒,它們是這樣工作的:守護執行緒一般是一個等待客戶請求的伺服器, 如果沒有客戶 出請求,它就在那等著。如果你設定一個執行緒為守護執行緒,就表示你在說這個執行緒 是不重要的,在程式退出的時候,不用等待這個執行緒退出。
如果你的主執行緒要退出的時候,不用等待那些子執行緒完成,那就設定這些執行緒的 daemon 屬性。 即,線上程開始(呼叫 thread.start())之前,呼叫 setDaemon()函式設定執行緒的 daemon 標誌 (thread.setDaemon(True))就表示這個執行緒“不重要”
如果你想要等待子執行緒完成再退出,那就什麼都不用做,或者顯式地呼叫 thread.setDaemon(False)以保證其 daemon 標誌為 False。你可以呼叫 thread.isDaemon()函式來判 斷其 daemon 標誌的值。新的子執行緒會繼承其父執行緒的 daemon 標誌。整個 Python 會在所有的非守護 執行緒退出後才會結束,即程式中沒有非守護執行緒存在的時候才結束。
Thread 類
Thread類提供了以下方法:
- run(): 用以表示執行緒活動的方法。
- start():啟動執行緒活動。
- join([time]): 等待至執行緒中止。這阻塞呼叫執行緒直至執行緒的join() 方法被呼叫中止-正常退出或者丟擲未處理的異常-或者是可選的超時發生。
- is_alive(): 返回執行緒是否活動的。
- name(): 設定/返回執行緒名。
- daemon(): 返回/設定執行緒的 daemon 標誌,一定要在呼叫 start()函式前設定
用 Thread 類,你可以用多種方法來建立執行緒。我們在這裡介紹三種比較相像的方法。
- 建立一個Thread的例項,傳給它一個函式
- 建立一個Thread的例項,傳給它一個可呼叫的類物件
- 從Thread派生出一個子類,建立一個這個子類的例項
下邊是三種不同方式的建立執行緒的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#! -*- coding: utf-8 -*- # 建立一個Thread的例項,傳給它一個函式 import threading from time import sleep, time loops = [4, 2] def loop(nloop, nsec, lock): print('start loop %s at: %s' % (nloop, time())) sleep(nsec) print('loop %s done at: %s' % (nloop, time())) # 每個執行緒都會被分配一個事先已經獲得的鎖,在 sleep()的時間到了之後就釋放 相應的鎖以通知主執行緒,這個執行緒已經結束了。 def main(): print('starting at:', time()) threads = [] nloops = range(len(loops)) for i in nloops: t = threading.Thread(target=loop, args=(i, loops[i])) threads.append(t) for i in nloops: # start threads threads[i].start() for i in nloops: # wait for all # join()會等到執行緒結束,或者在給了 timeout 引數的時候,等到超時為止。 # 使用 join()看上去 會比使用一個等待鎖釋放的無限迴圈清楚一些(這種鎖也被稱為"spinlock") threads[i].join() # threads to finish print('all DONE at:', time()) if __name__ == '__main__': main() |
與傳一個函式很相似的另一個方法是在建立執行緒的時候,傳一個可呼叫的類的例項供執行緒啟動 的時候執行——這是多執行緒程式設計的一個更為物件導向的方法。相對於一個或幾個函式來說,由於類 物件裡可以使用類的強大的功能,可以儲存更多的資訊,這種方法更為靈活
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
#! -*- coding: utf-8 -*- # 建立一個 Thread 的例項,傳給它一個可呼叫的類物件 from threading import Thread from time import sleep, time loops = [4, 2] class ThreadFunc(object): def __init__(self, func, args, name=""): self.name = name self.func = func self.args = args def __call__(self): # 建立新執行緒的時候,Thread 物件會呼叫我們的 ThreadFunc 物件,這時會用到一個特殊函式 __call__()。 self.func(*self.args) def loop(nloop, nsec): print('start loop %s at: %s' % (nloop, time())) sleep(nsec) print('loop %s done at: %s' % (nloop, time())) def main(): print('starting at:', time()) threads = [] nloops = range(len(loops)) for i in nloops: t = Thread(target=ThreadFunc(loop, (i, loops[i]), loop.__name__)) threads.append(t) for i in nloops: # start threads threads[i].start() for i in nloops: # wait for all # join()會等到執行緒結束,或者在給了 timeout 引數的時候,等到超時為止。 # 使用 join()看上去 會比使用一個等待鎖釋放的無限迴圈清楚一些(這種鎖也被稱為"spinlock") threads[i].join() # threads to finish print('all DONE at:', time()) if __name__ == '__main__': main() |
最後一個例子介紹如何子類化 Thread 類,這與上一個例子中的建立一個可呼叫的類非常像。使 用子類化建立執行緒(第 29-30 行)使程式碼看上去更清晰明瞭。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
#! -*- coding: utf-8 -*- # 建立一個 Thread 的例項,傳給它一個可呼叫的類物件 from threading import Thread from time import sleep, time loops = [4, 2] class MyThread(Thread): def __init__(self, func, args, name=""): super(MyThread, self).__init__() self.name = name self.func = func self.args = args def getResult(self): return self.res def run(self): # 建立新執行緒的時候,Thread 物件會呼叫我們的 ThreadFunc 物件,這時會用到一個特殊函式 __call__()。 print 'starting', self.name, 'at:', time() self.res = self.func(*self.args) print self.name, 'finished at:', time() def loop(nloop, nsec): print('start loop %s at: %s' % (nloop, time())) sleep(nsec) print('loop %s done at: %s' % (nloop, time())) def main(): print('starting at:', time()) threads = [] nloops = range(len(loops)) for i in nloops: t = MyThread(loop, (i, loops[i]), loop.__name__) threads.append(t) for i in nloops: # start threads threads[i].start() for i in nloops: # wait for all # join()會等到執行緒結束,或者在給了 timeout 引數的時候,等到超時為止。 # 使用 join()看上去 會比使用一個等待鎖釋放的無限迴圈清楚一些(這種鎖也被稱為"spinlock") threads[i].join() # threads to finish print('all DONE at:', time()) if __name__ == '__main__': main() |
除了各種同步物件和執行緒物件外,threading 模組還 供了一些函式。
- active_count(): 當前活動的執行緒物件的數量
- current_thread(): 返回當前執行緒物件
- enumerate(): 返回當前活動執行緒的列表
- settrace(func): 為所有執行緒設定一個跟蹤函式
- setprofile(func): 為所有執行緒設定一個 profile 函式
Lock & RLock
原語鎖定是一個同步原語,狀態是鎖定或未鎖定。兩個方法acquire()和release() 用於加鎖和釋放鎖。
RLock 可重入鎖是一個類似於Lock物件的同步原語,但同一個執行緒可以多次呼叫。
Lock 不支援遞迴加鎖,也就是說即便在同 執行緒中,也必須等待鎖釋放。通常建議改 RLock, 它會處理 “owning thread” 和 “recursion level” 狀態,對於同 執行緒的多次請求鎖 為,只累加
計數器。每次調 release() 將遞減該計數器,直到 0 時釋放鎖,因此 acquire() 和 release() 必須 要成對出現。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from time import sleep from threading import current_thread, Thread lock = Rlock() def show(): with lock: print current_thread().name, i sleep(0.1) def test(): with lock: for i in range(3): show(i) for i in range(2): Thread(target=test).start() |
Event
事件用於線上程間通訊。一個執行緒發出一個訊號,其他一個或多個執行緒等待。
Event 通過通過 個內部標記來協調多執行緒運 。 法 wait() 阻塞執行緒執 ,直到標記為 True。 set() 將標記設為 True,clear() 更改標記為 False。isSet() 用於判斷標記狀態。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from threading import Event def test_event(): e = Event() def test(): for i in range(5): print 'start wait' e.wait() e.clear() # 如果不呼叫clear(),那麼標記一直為 True,wait()就不會發生阻塞行為 print i Thread(target=test).start() return e e = test_event() |
Condition
條件變數和 Lock 引數一樣,也是一個,也是一個同步原語,當需要執行緒關注特定的狀態變化或事件的發生時使用這個鎖定。
可以認為,除了Lock帶有的鎖定池外,Condition還包含一個等待池,池中的執行緒處於狀態圖中的等待阻塞狀態,直到另一個執行緒呼叫notify()/notifyAll()通知;得到通知後執行緒進入鎖定池等待鎖定。
構造方法:
Condition([lock/rlock])
Condition 有以下這些方法:
- acquire([timeout])/release(): 呼叫關聯的鎖的相應方法。
- wait([timeout]): 呼叫這個方法將使執行緒進入Condition的等待池等待通知,並釋放鎖。使用前執行緒必須已獲得鎖定,否則將丟擲異常。
- notify(): 呼叫這個方法將從等待池挑選一個執行緒並通知,收到通知的執行緒將自動呼叫acquire()嘗試獲得鎖定(進入鎖定池);其他執行緒仍然在等待池中。呼叫這個方法不會釋放鎖定。使用前執行緒必須已獲得鎖定,否則將丟擲異常。
- notifyAll(): 呼叫這個方法將通知等待池中所有的執行緒,這些執行緒都將進入鎖定池嘗試獲得鎖定。呼叫這個方法不會釋放鎖定。使用前執行緒必須已獲得鎖定,否則將丟擲異常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
from threading import Condition, current_thread, Thread con = Condition() def tc1(): with con: for i in range(5): print current_thread().name, i sleep(0.3) if i == 3: con.wait() def tc2(): with con: for i in range(5): print current_thread().name, i sleep(0.1) con.notify() Thread(target=tc1).start() Thread(target=tc2).start() Thread-1 0 Thread-1 1 Thread-1 2 Thread-1 3 # 讓出鎖 Thread-2 0 Thread-2 1 Thread-2 2 Thread-2 3 Thread-2 4 Thread-1 4 # 重新獲取鎖,繼續執 |
只有獲取鎖的執行緒才能呼叫 wait() 和 notify(),因此必須在鎖釋放前呼叫。
當 wait() 釋放鎖後,其他執行緒也可進入 wait 狀態。notifyAll() 啟用所有等待執行緒,讓它們去搶鎖然後完成後續執行。
生產者-消費者問題和 Queue 模組
現在我們用一個經典的(生產者消費者)例子來介紹一下 Queue模組。
生產者消費者的場景是: 生產者生產貨物,然後把貨物放到一個佇列之類的資料結構中,生產貨物所要花費的時間無法預先確定。消費者消耗生產者生產的貨物的時間也是不確定的。
常用的 Queue 模組的屬性:
- queue(size): 建立一個大小為size的Queue物件。
- qsize(): 返回佇列的大小(由於在返回的時候,佇列可能會被其它執行緒修改,所以這個值是近似值)
- empty(): 如果佇列為空返回 True,否則返回 False
- full(): 如果佇列已滿返回 True,否則返回 False
- put(item,block=0): 把item放到佇列中,如果給了block(不為0),函式會一直阻塞到佇列中有空間為止
- get(block=0): 從佇列中取一個物件,如果給了 block(不為 0),函式會一直阻塞到佇列中有物件為止
Queue 模組可以用來進行執行緒間通訊,讓各個執行緒之間共享資料。
現在,我們建立一個佇列,讓 生產者(執行緒)把新生產的貨物放進去供消費者(執行緒)使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
#! -*- coding: utf-8 -*- from Queue import Queue from random import randint from time import sleep, time from threading import Thread class MyThread(Thread): def __init__(self, func, args, name=""): super(MyThread, self).__init__() self.name = name self.func = func self.args = args def getResult(self): return self.res def run(self): # 建立新執行緒的時候,Thread 物件會呼叫我們的 ThreadFunc 物件,這時會用到一個特殊函式 __call__()。 print 'starting', self.name, 'at:', time() self.res = self.func(*self.args) print self.name, 'finished at:', time() # writeQ()和 readQ()函式分別用來把物件放入佇列和消耗佇列中的一個物件。在這裡我們使用 字串'xxx'來表示佇列中的物件。 def writeQ(queue): print 'producing object for Q...' queue.put('xxx', 1) print "size now", queue.qsize() def readQ(queue): queue.get(1) print("consumed object from Q... size now", queue.qsize()) def writer(queue, loops): # writer()函式只做一件事,就是一次往佇列中放入一個物件,等待一會,然後再做同樣的事 for i in range(loops): writeQ(queue) sleep(1) def reader(queue, loops): # reader()函式只做一件事,就是一次從佇列中取出一個物件,等待一會,然後再做同樣的事 for i in range(loops): readQ(queue) sleep(randint(2, 5)) # 設定有多少個執行緒要被執行 funcs = [writer, reader] nfuncs = range(len(funcs)) def main(): nloops = randint(10, 20) q = Queue(32) threads = [] for i in nfuncs: t = MyThread(funcs[i], (q, nloops), funcs[i].__name__) threads.append(t) for i in nfuncs: threads[i].start() for i in nfuncs: threads[i].join() print threads[i].getResult() print 'all DONE' if __name__ == '__main__': main() |
FAQ
程式與執行緒。執行緒與程式的區別是什麼?
程式(有時被稱為重量級程式)是程式的一次 執行。每個程式都有自己的地址空間,記憶體,資料棧以及其它記錄其執行軌跡的輔助資料。
執行緒(有時被稱為輕量級程式)跟程式有些相似,不同的是,所有的執行緒執行在同一個程式中, 共享相同的執行環境。它們可以想像成是在主程式或“主執行緒”中並行執行的“迷你程式”。
這篇文章很好的解釋了 執行緒和程式的區別,推薦閱讀: http://www.ruanyifeng.com/blo…
Python 的執行緒。在 Python 中,哪一種多執行緒的程式表現得更好,I/O 密集型的還是計算 密集型的?
由於GIL的緣故,對所有面向 I/O 的(會呼叫內建的作業系統 C 程式碼的)程式來說,GIL 會在這個 I/O 呼叫之 前被釋放,以允許其它的執行緒在這個執行緒等待 I/O 的時候執行。如果某執行緒並未使用很多 I/O 操作, 它會在自己的時間片內一直佔用處理器(和 GIL)。也就是說,I/O 密集型的 Python 程式比計算密集 型的程式更能充分利用多執行緒環境的好處。
執行緒。你認為,多 CPU 的系統與一般的系統有什麼大的不同?多執行緒的程式在這種系統上的表現會怎麼樣?
Python的執行緒就是C語言的一個pthread,並通過作業系統排程演算法進行排程(例如linux是CFS)。為了讓各個執行緒能夠平均利用CPU時間,python會計算當前已執行的微程式碼數量,達到一定閾值後就強制釋放GIL。而這時也會觸發一次作業系統的執行緒排程(當然是否真正進行上下文切換由作業系統自主決定)。
虛擬碼
1 2 3 4 5 6 |
while True: acquire GIL for i in 1000: do something release GIL /* Give Operating System a chance to do thread scheduling */ |
這種模式在只有一個CPU核心的情況下毫無問題。任何一個執行緒被喚起時都能成功獲得到GIL(因為只有釋放了GIL才會引發執行緒排程)。
但當CPU有多個核心的時候,問題就來了。從虛擬碼可以看到,從release GIL到acquire GIL之間幾乎是沒有間隙的。所以當其他在其他核心上的執行緒被喚醒時,大部分情況下主執行緒已經又再一次獲取到GIL了。這個時候被喚醒執行的執行緒只能白白的浪費CPU時間,看著另一個執行緒拿著GIL歡快的執行著。然後達到切換時間後進入待排程狀態,再被喚醒,再等待,以此往復惡性迴圈。
簡單的總結下就是:Python的多執行緒在多核CPU上,只對於IO密集型計算產生正面效果;而當有至少有一個CPU密集型執行緒存在,那麼多執行緒效率會由於GIL而大幅下降。