Python 線程(threading) 進程(multiprocessing)
最近學了兩個python庫,一個負責管理線程,一個負責管理進程,原來一直寫的都 是些單線程的程序,雖然web也關於並發和多涉及到線程,但都是框架管理的,學習>過后發現了解線程和進程對python的web開發也有一定幫助。下面先談談這對python對線程和進程的支持再談談對這兩個庫的應用。
python對線程的支持並不是非常好,所以你可以在很多文章上批評python的多線程的弊端,但是為什么python對多線程支持不好呢,為什么其他語言比如 C、java、c#靜態語言沒有這個弊端呢。
首先我們要知道python是一種解釋性語言,每段代碼都需要解釋器編譯運行,解釋器有很多種最主要的是CPython,其他還有IronPython和Jython,官方的是CPython解釋器,我們一般說對多線程支持不好的就是說的CPython解釋器(用的人最多就省略成python解釋器),python解釋器為什么對多線程支持不好呢,是因為GIL的存在,當然這個存在就是因為這門語言的的特性產生的。
GIL是什么呢,下面是官方的解釋
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.)
就是GIL是python的互斥鎖,簡單的理解就是代碼會鎖住python解釋器。理解代碼的鎖定是什么必須要先了解什么是多線程
多線程表示一個主線程,多個子線程,主線程是程序執行時系統自動給你申請的一個線程,而子線程我們可以理解為一個代碼塊,我們可以充分利用硬件的支持比如說多核,讓一個CPU執行主線程,其他CPU執行子線程,通過操作系統的虛擬內存技術讓所有線程共享相同代碼空間達到提高代碼效率的作用,我們可以通俗的把一個進程比作一輛火車,車廂頭為主線程,每節車廂為子線程,只要你車廂(子線程)越多,你運的貨物也越多,但是也要考慮硬件的方面,
了解完多線程是什么我們就可以解釋GIL對多核CPU工作性能的影響了,在單核CPU里面,主線程在釋放GIL的時候,把CPU讓給子線程,子線程代碼塊得到GIL,然后執行,這樣就能充分利用CPU,這個GIL對單核性能的發揮沒有影響,能得到100%的利用,但是在多核的的時候就有問題了,假如主線程的代碼一直需要解釋器來執行, 比如說下面
GIL.acquire()
try:
while True:
do_something()
finally:
GIL.release()
主線程代碼對GIL的鎖定和解開只間隔很小的一個系統時間,子線程在其他CPU核心得到GIL解開后CPU的調度命令后才能被喚醒,但是當喚醒后,主線程的代碼又鎖了GIL,然后只能等待主線程下次調度命令,但是到了切換時間又切換回去到待調整狀態,一直處於喚醒,等待的惡性循環,多核的功能完全沒有發揮出來而且還比單核更加差,所以python因為GIL的存在對密集型的線程支持不佳,但是假如主線程是在執行想web這樣等待用戶輸入,而不是每分每秒都在使用解釋器執行代碼,多線程的優勢就能發揮出來。
解決方案
GIL作為解釋器的一個Bug一樣的存在,我們也有一定的解決方法,開線程,和用Ctype繞過解釋器是我們一般的解決方法,你想了解更多可以看這個 接下來主要解紹用multiprocessing來繞過多線程的瓶頸
線程鎖和進程鎖
為了實現線程安全,我們也要借助鎖的存在,我們先用下面的代碼來驗證一下多線程對於線程安全的問題。我們聲明一個線程鎖 threading.Lock()
,
class Counter(object):
def __init__(self, start=0):
self.lock = threading.Lock()
self.value = start
def increment(self):
logging.debug('Waiting for lock')
self.lock.acquire()
try:
if self.value < 8:
time.sleep(2) # 模擬負載
logging.debug('Acquired lock')
self.value = self.value + 1
finally:
self.lock.release()
def worker(c):
for i in range(2):
pause = random.random()
logging.debug('Sleeping %0.02f', pause)
time.sleep(pause)
c.increment()
logging.debug('Done')
counter = Counter()
for i in range(20):
t = threading.Thread(target=worker,args=(counter,))
t.start()
main_thread = threading.currentThread()
for t in threading.enumerate():
if t is not main_thread:
t.join() # 保護線程
logging.debug('Counter:%d', counter.value) #得到value值
我們運行之后得到counter.value
值為8,這很好理解因為我們限制了它的大小小於8時才自增1,但是如果我們把鎖去掉呢,我們把self.lock.acquire()
self.lock.release()
都注釋掉,得到的結果卻是一個21,而且每次運行的結果都可能不一樣,由於線程在實現自增的時候有一定的時間(time.sleep(2)
),所以當多個進程執行的時候當他們從堆棧上取到counter.value
值都為7時,這時候他們都滿足 counter.value
小於8,所以都執行了自增,在系統負載2秒之間(time.sleep(2)
)有多少個線程執行就會逃過我們給他的限制,這樣就造成了線程的不安全,但是我們給他加上鎖之后,無論開多少個線程,最終結果都是8。在python里面我們線程鎖和進程鎖我們可以看做是同一種東西。
ps:當同一線程相互爭奪鎖時,失敗的會進出線程隊列等待鎖解開。
線程進程工作方式
單行
單行主要通過鎖來實現,線程通過鎖
threading.Lock()
對象創造鎖,進程通過multiprocessing.Lock()
對象創建進程鎖,單行操作一般都是對共享數據修改的一種保護。
並行
並行操作是一般是對數據的一種共享,一般不對公共數據涉及修改,我們可以創造很多線程和進程一起並行操作,也可以限制線程和進程的並行數量,兩種方式選擇主要是判斷代碼類型是I/O密集還是線程密集型的。如何限制並行數量我們可以通過
threading.Semaphore(sizenum)
(進程為multiprocessing.Semaphore(sizenum)
)我們可以控制對共享的線程數量。進程提供了一個進程池的類型(multiprocessing.Pool
),我們可以創建一個維護了一定程的進程池,但是他同時並行的數量並沒有控制,只是幫我們創建了這個進程池,每個進程並不是只執行一個任務,可能執行多個方法通過一個進程.
單行混合並行
單行和並行混合我們可以通過在代碼中設置鎖來實現,當然python給我們提供了兩種對象來實現單行和並行的控制,線程的是
threading.Event()
和threading.Condition()
,進程的是multiprocessing.Event()
和multiprocessing.Condition()
兩種對象都是提供了一種命令指令,但是Event對象可以用來判斷命令是否下達而做出相應的反應,而Condition對象更傾向於當命令下達后才執行並行的操作。
線程和進程通信方式
當我們想讓線程和進程共同執行一些固定的任務,我們就需要線程和進程之間能夠通信,線程和進程通信我們使用隊列(
Queue
),進程和線程的Queue
有點差異,就是進程Queue
傳遞的對象必須pickle化,而且為了能夠使用join()
(保護進程)task_done
(通知任務完成),我們一般使用JoinableQueue
代替Queue
在進程中。
Queue對象之間通過put
和get
通信,我們把任務put上去,Queue
自動分配給當前的線程或進程, 這樣就能實現對任務的流水作業話。
引用
12/26/2015 10:50:21 PM GIL維基資料