python多線程
進程和線程是操作系統(tǒng)領(lǐng)域非常重要的概念猬仁,對于二者之間的聯(lián)系與區(qū)別,本文不做過多闡述震肮,這方面資料網(wǎng)上有非常多,如有需要請先自行查閱胰坟。
1 基礎(chǔ)知識之“雞肋”的python多線程和GIL
Python是一種解釋型語言因篇,而對于python主流也是官方的解釋器CPython來說泞辐,每一個進程都會持有一個全局解釋鎖GIL(Global Interpreter Lock)。一個進程運行python代碼時竞滓,同一時刻只能有一個線程獲得這個GIL鎖咐吼,如果該進程內(nèi)的其他線程想要運行時,就必須要等待當(dāng)前線程阻塞的時候釋放全局解釋鎖商佑,而不能多個線程同時運行在CPU锯茄。這點和java多線程運行在多核是不同的。正因如此茶没,導(dǎo)致了python多線程的效率并不能提高單線程執(zhí)行程序的效率肌幽。
代碼如下所示
import time
# 單線程運行 3 * 100000 次乘法
a = 1
b = 1
c = 1
begin = time.time()
for i in range(100000):
a *=2
b *=2
c *=2
end = time.time()
print("共消耗時間 %.2f 秒" % (end - begin))
輸出為
共消耗時間 0.61 秒
import time
import threading
# 創(chuàng)建3個線程各運行 100000 次乘法
def multiply_op(n):
for i in range(100000):
n *= 2
a = 1
b = 1
c = 1
begin = time.time()
# 創(chuàng)建3個線程分別對a,b抓半,c運行100000次乘法
t1 = threading.Thread(target=multiply_op, args=(a,))
t2 = threading.Thread(target=multiply_op, args=(b,))
t3 = threading.Thread(target=multiply_op, args=(c,))
# 啟動三個線程
t1.start()
t2.start()
t3.start()
# 等待三個線程運行結(jié)束
t1.join()
t2.join()
t3.join()
end = time.time()
print("共消耗時間 %.2f 秒" % (end - begin))
輸出為
共消耗時間 0.61 秒
上述多線程實現(xiàn)的代碼如果看不懂的話沒有關(guān)系喂急,因為我會在后面進行講解,在這里我們只需要觀察到結(jié)果笛求,也就是多線程實現(xiàn)的效率并沒有對單線程有所提高廊移,這是因為多個線程在輪流獲得GIL,并不是并發(fā)執(zhí)行探入。事實上多線程因為增加了各個線程之間切換時調(diào)度資源的時間狡孔,反而比起單線程程序效率有所下降。
這樣看來蜂嗽,python中的多線程確實如人們所說十分“雞肋”苗膝,但是既然如此“雞肋”,是不是python多線程就真的一無是處呢徒爹?答案當(dāng)然是否定的荚醒。python多線程經(jīng)常應(yīng)用于IO頻繁的程序,例如爬蟲程序隆嗅,我們都知道爬蟲程序經(jīng)常會在請求網(wǎng)站后自身阻塞等待回送請求界阁,這就是一個很好的進行線程調(diào)度的時機。
2 python多線程實戰(zhàn)
Python的標準庫提供了兩個模塊:thread和threading胖喳,thread是低級模塊泡躯,threading是高級模塊,對thread進行了封裝丽焊。絕大多數(shù)情況下较剃,我們只需要使用threading這個高級模塊。
2.1 簡單實例
import time, threading
def loop() -> None:
print('thread ', threading.current_thread().name, ' is running...')
n = 0
while n < 5:
n = n + 1
print('thread ', threading.current_thread().name, ': n: ', n)
time.sleep(1)
print('thread', threading.current_thread().name, ' ended.')
print('thread ', threading.current_thread().name, ' is running...')
t = threading.Thread(target=loop, name='LoopThread')
t.start()
# join()函數(shù)是讓其他方法阻塞而等待調(diào)用該方法的線程運行結(jié)束
# 結(jié)束可以是正臣冀。或非正常終止写穴,或者是通過傳入timeout參數(shù)設(shè)定其他線程阻塞的時間
t.join()
print('thread', threading.current_thread().name, ' ended.')
以上是實現(xiàn)python多線程的一個簡單樣例,其中程序使用主線程運行程序雌贱,直到我們利用threading模塊創(chuàng)建了一個新的線程t啊送,創(chuàng)建線程調(diào)用的函數(shù)傳入的參數(shù)中偿短,target參數(shù)就是我們要讓這個線程執(zhí)行的函數(shù),name指定的就是這個線程的名稱馋没,創(chuàng)建完成后昔逗,我們要使用start()函數(shù)啟動它,這時候如果沒有其他操作的話篷朵,該線程將與主線程一起運行勾怒,共同請求GIL,而我們在名為LoopThread的線程啟動后声旺,緊接著調(diào)用了join()函數(shù)笔链,這個函數(shù)的作用在于將使其他此刻存在的線程等待這個線程運行結(jié)束后再繼續(xù)執(zhí)行。所以我們的輸出結(jié)果如下所示:
thread MainThread is running...
thread LoopThread is running...
thread LoopThread : n: 1
thread LoopThread : n: 2
thread LoopThread : n: 3
thread LoopThread : n: 4
thread LoopThread : n: 5
thread LoopThread ended.
thread MainThread ended.
2.2 python多線程之自旋鎖艾少、可重入鎖
首先恭喜你已經(jīng)掌握了基本的python多線程開發(fā)卡乾,但是你還不能高興的太早,因為還有許許多多的問題等待我們?nèi)ソ鉀Q缚够。對操作系統(tǒng)稍有了解的同學(xué)們應(yīng)該都明白幔妨,并發(fā)程序中最重要的就是資源共享問題,就比如我們兩個線程在共享同一個變量的時候谍椅,如何做到不發(fā)生錯誤误堡。
首先來看一段代碼:
import threading
balance = 0
# 操作銀行賬戶中的余額
def op_cash(n):
global balance
# 存錢
balance = balance + n
# 取錢
balance = balance - n
def run_thread(n):
for i in range(10000000):
op_cash(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)
這段程序模擬了兩個人對同一個銀行賬戶進行存取款的操作,第一個人t1每次存五塊錢并且取五塊錢雏吭,而第二個人每次存八塊錢取八塊錢锁施。那么你認為結(jié)果應(yīng)該是多少呢?理想情況下杖们,我們覺得結(jié)果應(yīng)當(dāng)為0悉抵。但是我們來看一下真正的輸出結(jié)果:
40
是不是感到很驚訝呢?這個讓人驚訝的結(jié)果就是由于我們在多線程對一個共享變量進行操作時線程不安全導(dǎo)致的摘完。比如現(xiàn)在余額是0姥饰,兩個人同時對余額進行操作,第一個人存五塊錢并且先完成了操作孝治,現(xiàn)在余額是五塊錢列粪,但是第二個人存八塊錢,這樣就直接把第一個人存的五塊錢覆蓋掉谈飒,現(xiàn)在的余額就是八塊錢岂座。
所以這就是我們這一節(jié)要急待解決的問題。其實看到這里有很多同學(xué)其實已經(jīng)有了解決問題的答案杭措,那就是——加鎖费什。完全正確,那我們就馬上來探索一下python多線程中的鎖吧手素。
同樣我們先寫一段簡單的代碼進行講解:
import threading
balance = 0
lock = threading.Lock()
# 操作銀行賬戶中的余額
def op_cash(n):
global balance
# 存錢
balance = balance + n
# 取錢
balance = balance - n
def run_thread(n):
for i in range(10000000):
with lock:
op_cash(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)
我們首先利用threading模塊獲得了一個鎖lock吕喘,隨后在執(zhí)行操作賬戶余額的函數(shù)時加了鎖赘那,這樣就保證了多個線程同一時刻只能有一個線程進行賬戶余額的操作,這就保證了線程的安全氯质,保證了共享變量不會出現(xiàn)我們意想不到的結(jié)果。是不是很簡單呢~
在你洋洋得意學(xué)會加鎖的時候祠斧,我們再來考慮一段代碼:
import threading
lock = threading.Lock()
def unreentrancelock_caller():
with lock:
print('unreentrancelock_caller')
print('Thread ', threading.current_thread().name, ' with lock')
unreentrancelock_callee()
def unreentrancelock_callee():
with lock:
print('unreentrancelock_callee')
print('Thread ', threading.current_thread().name, ' with lock')
t1 = threading.Thread(name='unreentrancelock_caller', target=unreentrancelock_caller)
t1.start()
t1.join()
print('End.')
其中我們執(zhí)行unreentrancelock_caller()函數(shù)闻察,在這個函數(shù)中,我們將繼續(xù)調(diào)用unreentrancelock_callee()函數(shù)琢锋。隨后我們執(zhí)行這段程序辕漂,發(fā)現(xiàn)輸出是這樣的:
unreentrancelock_caller
Thread unreentrancelock_caller with lock
但是注意觀察,程序執(zhí)行結(jié)束了嗎吴超?并沒有钉嘹。這是為什么呢?這是因為我們這里獲得的lock是一個不可重入鎖鲸阻,也就是自旋鎖跋涣,當(dāng)我們的程序執(zhí)行到unreentrancelock_callee()中請求lock的時候,我們發(fā)現(xiàn)我們其實已經(jīng)在unreentrancelock_caller()中獲取過一次了鸟悴,所以現(xiàn)在lock在caller手里陈辱,callee自然獲取不到,這就導(dǎo)致了细诸,caller要想繼續(xù)執(zhí)行沛贪,就必須等待callee執(zhí)行完畢,但是callee要想繼續(xù)執(zhí)行震贵,就必須等待caller釋放lock利赋,這就造成了死鎖,從而程序掛起猩系。
那么媚送,我們是否有辦法讓這段程序繼續(xù)執(zhí)行下去呢?答案是使用可重入鎖蝙眶。
import threading
rlock = threading.RLock()
def reentrancelock_caller():
with rlock:
print('reentrancelock_caller')
print('Thread ', threading.current_thread().name, ' with rlock')
reentrancelock_callee()
def reentrancelock_callee():
with rlock:
print('reentrancelock_callee')
print('Thread ', threading.current_thread().name, ' with rlock')
t1 = threading.Thread(name='reentrancelock_caller', target=reentrancelock_caller)
t1.start()
t1.join()
print('End.')
同樣季希,threading模塊為我們封裝了RLock,我們可以直接利用獲得到的可重入鎖進行使用幽纷,執(zhí)行結(jié)果如下
reentrancelock_caller
Thread reentrancelock_caller with rlock
reentrancelock_callee
Thread reentrancelock_caller with rlock
End.
是不是感覺可重入鎖的使用也很方便呢~
2.3 python多線程之定時任務(wù)
在threading模塊中式塌,博主自認為還有一個比較實用的功能拿來分享一下,那就是Timer執(zhí)行定時任務(wù)友浸,比如我們當(dāng)前有一個任務(wù)我們不需要他馬上執(zhí)行峰尝,而是定時執(zhí)行,我們就可以用到它了~
import time
from threading import Timer
def timer_test():
print("共經(jīng)歷時間 %.2f 秒" % (time.time() - begin))
my_timer = Timer(5, timer_test)
begin = time.time()
my_timer.start()
在新建我們的my_timer實例時收恢,傳入的第一個參數(shù)為需要定時的時間武学,而第二個參數(shù)是我們要執(zhí)行的函數(shù)名祭往。
輸出為
共經(jīng)歷時間 5.00 秒
2.4 python多線程總結(jié)
通過本文的介紹,相信你已經(jīng)對python多線程的知識有了一定的了解火窒,正如我們所見硼补,python多線程并不能像java多線程一樣同時運行在CPU多個核心上,其運用的場合主要為IO密集型程序熏矿。
那么也許你有這樣的問題已骇,那面對計算密集型的程序時我們該怎么辦呢?我們是不是必須要使用java解決問題呢票编?答案是我們可以使用python多進程褪储,對于這一部分內(nèi)容,博主將在下一篇博文進行討論慧域。