多線程
多任務可以由多進程完成斧抱,也可以由一個進程內(nèi)的多線程完成。進程是由若干線程組成的渐溶,一個進程至少有一個線程。Python的標準庫提供了兩個模塊:_thread和threading茎辐,_thread是低級模塊宪郊,threading是高級模塊拖陆,對_thread進行了封裝弛槐。絕大多數(shù)情況下,我們只需要使用threading這個高級模塊依啰。
啟動一個線程就是把一個函數(shù)傳入并創(chuàng)建Thread實例乎串,然后調(diào)用start()開始執(zhí)行:
import time, threading
# 新線程執(zhí)行的代碼:
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)
執(zhí)行結果如下:
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.
由于任何進程默認就會啟動一個線程,我們把該線程稱為主線程速警,主線程又可以啟動新的線程艰争,Python的threading模塊有個current_thread()函數(shù),它永遠返回當前線程的實例桂对。主線程實例的名字叫MainThread鸠匀,子線程的名字在創(chuàng)建時指定蕉斜,我們用LoopThread命名子線程。名字僅僅在打印時用來顯示缀棍,完全沒有其他意義宅此,如果不起名字Python就自動給線程命名為Thread-1,Thread-2……
- Lock(重要)
多線程和多進程最大的不同在于爬范,多進程中父腕,同一個變量,各自有一份拷貝存在于每個進程中青瀑,互不影響,而多線程中斥难,所有變量都由所有線程共享,所以哑诊,任何一個變量都可以被任何一個線程修改群扶,因此镀裤,線程之間共享數(shù)據(jù)最大的危險在于多個線程同時改一個變量竞阐,把內(nèi)容給改亂了暑劝。
來看看多個線程同時操作一個變量怎么把內(nèi)容給改亂了:
import time, threading
# 假定這是你的銀行存款:
balance = 0
def change_it(n):
# 先存后取骆莹,結果應該為0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(100000):
change_it(n)
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
我們定義了一個共享變量balance铃岔,初始值為0汪疮,并且啟動兩個線程,先存后取毁习,理論上結果應該為0,但是纺且,由于線程的調(diào)度是由操作系統(tǒng)決定的,當t1载碌、t2交替執(zhí)行時猜嘱,只要循環(huán)次數(shù)足夠多,balance的結果就不一定是0了朗伶。
原因是因為高級語言的一條語句在CPU執(zhí)行時是若干條語句,即使一個簡單的計算:
balance = balance + n
也分兩步:
計算balance + n论皆,存入臨時變量中益楼;
將臨時變量的值賦給balance点晴。
也就是可以看成:
x = balance + n
balance = x
由于x是局部變量感凤,兩個線程各自都有自己的x粒督,當代碼正常執(zhí)行時:
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2 # balance = 0
結果 balance = 0
但是t1和t2是交替運行的陪竿,如果操作系統(tǒng)以下面的順序執(zhí)行t1屠橄、t2:
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance - 8 # x2 = 0 - 8 = -8
t2: balance = x2 # balance = -8
結果 balance = -8
究其原因族跛,是因為修改balance需要多條語句仇矾,而執(zhí)行這幾條語句時庸蔼,線程可能中斷贮匕,從而導致多個線程把同一個對象的內(nèi)容改亂了姐仅。
兩個線程同時一存一取刻盐,就可能導致余額不對掏膏,你肯定不希望你的銀行存款莫名其妙地變成了負數(shù),所以敦锌,我們必須確保一個線程在修改balance的時候馒疹,別的線程一定不能改。
如果我們要確保balance計算正確乙墙,就要給change_it()上一把鎖颖变,當某個線程開始執(zhí)行change_it()時,我們說听想,該線程因為獲得了鎖腥刹,因此其他線程不能同時執(zhí)行change_it()汉买,只能等待衔峰,直到鎖被釋放后,獲得該鎖以后才能改垫卤。由于鎖只有一個威彰,無論多少線程穴肘,同一時刻最多只有一個線程持有該鎖歇盼,所以梢褐,不會造成修改的沖突旺遮。創(chuàng)建一個鎖就是通過threading.Lock()
來實現(xiàn):
balance = 0
lock = threading.Lock()
def run_thread(n):
for i in range(100000):
# 先要獲取鎖:
lock.acquire()
try:
# 放心地改吧:
change_it(n)
finally:
# 改完了一定要釋放鎖:
lock.release()
當多個線程同時執(zhí)行l(wèi)ock.acquire()時盈咳,只有一個線程能成功地獲取鎖边翼,然后繼續(xù)執(zhí)行代碼鱼响,其他線程就繼續(xù)等待直到獲得鎖為止组底。
獲得鎖的線程用完后一定要釋放鎖丈积,否則那些苦苦等待鎖的線程將永遠等待下去,成為死線程江滨。所以我們用try...finally來確保鎖一定會被釋放。
鎖的好處就是確保了某段關鍵代碼只能由一個線程從頭到尾完整地執(zhí)行厌均,壞處當然也很多,首先是阻止了多線程并發(fā)執(zhí)行棺弊,包含鎖的某段代碼實際上只能以單線程模式執(zhí)行,效率就大大地下降了模她。其次,由于可以存在多個鎖侈净,不同的線程持有不同的鎖尊勿,并試圖獲取對方持有的鎖時畜侦,可能會造成死鎖元扔,導致多個線程全部掛起夏伊,既不能執(zhí)行摇展,也無法結束,只能靠操作系統(tǒng)強制終止咏连。
- 多核CPU
如果你不幸擁有一個多核CPU,你肯定在想祟滴,多核應該可以同時執(zhí)行多個線程振惰。
如果寫一個死循環(huán)的話垄懂,會出現(xiàn)什么情況呢骑晶?
打開Mac OS X的Activity Monitor草慧,或者Windows的Task Manager桶蛔,都可以監(jiān)控某個進程的CPU使用率漫谷。
我們可以監(jiān)控到一個死循環(huán)線程會100%占用一個CPU仔雷。
如果有兩個死循環(huán)線程舔示,在多核CPU中碟婆,可以監(jiān)控到會占用200%的CPU惕稻,也就是占用兩個CPU核心若皱。
要想把N核CPU的核心全部跑滿享潜,就必須啟動N個死循環(huán)線程。
試試用Python寫個死循環(huán):
import threading, multiprocessing
def loop():
x = 0
while True:
x = x ^ 1
for i in range(multiprocessing.cpu_count()):
t = threading.Thread(target=loop)
t.start()
啟動與CPU核心數(shù)量相同的N個線程锻煌,在4核CPU上可以監(jiān)控到CPU占用率僅有102%妓布,也就是僅使用了一核宋梧。
但是用C匣沼、C++或Java來改寫相同的死循環(huán)捂龄,直接可以把全部核心跑滿释涛,4核就跑到400%倦沧,8核就跑到800%唇撬,為什么Python不行呢展融?
因為Python的線程雖然是真正的線程窖认,但解釋器執(zhí)行代碼時,有一個GIL鎖:Global Interpreter Lock扑浸,任何Python線程執(zhí)行前烧给,必須先獲得GIL鎖喝噪,然后础嫡,每執(zhí)行100條字節(jié)碼酝惧,解釋器就自動釋放GIL鎖榴鼎,讓別的線程有機會執(zhí)行晚唇。這個GIL全局鎖實際上把所有線程的執(zhí)行代碼都給上了鎖,所以哩陕,多線程在Python中只能交替執(zhí)行翁涤,即使100個線程跑在100核CPU上萌踱,也只能用到1個核。
GIL是Python解釋器設計的歷史遺留問題并鸵,通常我們用的解釋器是官方實現(xiàn)的CPython,要真正利用多核扔涧,除非重寫一個不帶GIL的解釋器。
所以枯夜,在Python中,可以使用多線程湖雹,但不要指望能有效利用多核咏闪。如果一定要通過多線程利用多核摔吏,那只能通過C擴展來實現(xiàn)鸽嫂,不過這樣就失去了Python簡單易用的特點征讲。
不過据某,也不用過于擔心诗箍,Python雖然不能利用多線程實現(xiàn)多核任務癣籽,但可以通過多進程實現(xiàn)多核任務。多個Python進程有各自獨立的GIL鎖筷狼,互不影響。