python多線程
進(jìn)程和線程是操作系統(tǒng)領(lǐng)域非常重要的概念,對于二者之間的聯(lián)系與區(qū)別,本文不做過多闡述,這方面資料網(wǎng)上有非常多,如有需要請先自行查閱。
1 基礎(chǔ)知識之“雞肋”的python多線程和GIL
Python是一種解釋型語言,而對于python主流也是官方的解釋器CPython來說,每一個(gè)進(jìn)程都會持有一個(gè)全局解釋鎖GIL(Global Interpreter Lock)。一個(gè)進(jìn)程運(yùn)行python代碼時(shí),同一時(shí)刻只能有一個(gè)線程獲得這個(gè)GIL鎖,如果該進(jìn)程內(nèi)的其他線程想要運(yùn)行時(shí),就必須要等待當(dāng)前線程阻塞的時(shí)候釋放全局解釋鎖,而不能多個(gè)線程同時(shí)運(yùn)行在CPU。這點(diǎn)和java多線程運(yùn)行在多核是不同的。正因如此,導(dǎo)致了python多線程的效率并不能提高單線程執(zhí)行程序的效率。
代碼如下所示
import time
# 單線程運(yùn)行 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("共消耗時(shí)間 %.2f 秒" % (end - begin))
輸出為
共消耗時(shí)間 0.61 秒
import time
import threading
# 創(chuàng)建3個(gè)線程各運(yùn)行 100000 次乘法
def multiply_op(n):
for i in range(100000):
n *= 2
a = 1
b = 1
c = 1
begin = time.time()
# 創(chuàng)建3個(gè)線程分別對a,b,c運(yùn)行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,))
# 啟動三個(gè)線程
t1.start()
t2.start()
t3.start()
# 等待三個(gè)線程運(yùn)行結(jié)束
t1.join()
t2.join()
t3.join()
end = time.time()
print("共消耗時(shí)間 %.2f 秒" % (end - begin))
輸出為
共消耗時(shí)間 0.61 秒
上述多線程實(shí)現(xiàn)的代碼如果看不懂的話沒有關(guān)系,因?yàn)槲視诤竺孢M(jìn)行講解,在這里我們只需要觀察到結(jié)果,也就是多線程實(shí)現(xiàn)的效率并沒有對單線程有所提高,這是因?yàn)槎鄠€(gè)線程在輪流獲得GIL,并不是并發(fā)執(zhí)行。事實(shí)上多線程因?yàn)樵黾恿烁鱾€(gè)線程之間切換時(shí)調(diào)度資源的時(shí)間,反而比起單線程程序效率有所下降。
這樣看來,python中的多線程確實(shí)如人們所說十分“雞肋”,但是既然如此“雞肋”,是不是python多線程就真的一無是處呢?答案當(dāng)然是否定的。python多線程經(jīng)常應(yīng)用于IO頻繁的程序,例如爬蟲程序,我們都知道爬蟲程序經(jīng)常會在請求網(wǎng)站后自身阻塞等待回送請求,這就是一個(gè)很好的進(jìn)行線程調(diào)度的時(shí)機(jī)。
2 python多線程實(shí)戰(zhàn)
Python的標(biāo)準(zhǔn)庫提供了兩個(gè)模塊:thread和threading,thread是低級模塊,threading是高級模塊,對thread進(jìn)行了封裝。絕大多數(shù)情況下,我們只需要使用threading這個(gè)高級模塊。
2.1 簡單實(shí)例
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)用該方法的線程運(yùn)行結(jié)束
# 結(jié)束可以是正?;蚍钦=K止,或者是通過傳入timeout參數(shù)設(shè)定其他線程阻塞的時(shí)間
t.join()
print('thread', threading.current_thread().name, ' ended.')
以上是實(shí)現(xiàn)python多線程的一個(gè)簡單樣例,其中程序使用主線程運(yùn)行程序,直到我們利用threading模塊創(chuàng)建了一個(gè)新的線程t,創(chuàng)建線程調(diào)用的函數(shù)傳入的參數(shù)中,target參數(shù)就是我們要讓這個(gè)線程執(zhí)行的函數(shù),name指定的就是這個(gè)線程的名稱,創(chuàng)建完成后,我們要使用start()函數(shù)啟動它,這時(shí)候如果沒有其他操作的話,該線程將與主線程一起運(yùn)行,共同請求GIL,而我們在名為LoopThread的線程啟動后,緊接著調(diào)用了join()函數(shù),這個(gè)函數(shù)的作用在于將使其他此刻存在的線程等待這個(gè)線程運(yùn)行結(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ā),但是你還不能高興的太早,因?yàn)檫€有許許多多的問題等待我們?nèi)ソ鉀Q。對操作系統(tǒng)稍有了解的同學(xué)們應(yīng)該都明白,并發(fā)程序中最重要的就是資源共享問題,就比如我們兩個(gè)線程在共享同一個(gè)變量的時(shí)候,如何做到不發(fā)生錯(cuò)誤。
首先來看一段代碼:
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)
這段程序模擬了兩個(gè)人對同一個(gè)銀行賬戶進(jìn)行存取款的操作,第一個(gè)人t1每次存五塊錢并且取五塊錢,而第二個(gè)人每次存八塊錢取八塊錢。那么你認(rèn)為結(jié)果應(yīng)該是多少呢?理想情況下,我們覺得結(jié)果應(yīng)當(dāng)為0。但是我們來看一下真正的輸出結(jié)果:
40
是不是感到很驚訝呢?這個(gè)讓人驚訝的結(jié)果就是由于我們在多線程對一個(gè)共享變量進(jìn)行操作時(shí)線程不安全導(dǎo)致的。比如現(xiàn)在余額是0,兩個(gè)人同時(shí)對余額進(jìn)行操作,第一個(gè)人存五塊錢并且先完成了操作,現(xiàn)在余額是五塊錢,但是第二個(gè)人存八塊錢,這樣就直接把第一個(gè)人存的五塊錢覆蓋掉,現(xiàn)在的余額就是八塊錢。
所以這就是我們這一節(jié)要急待解決的問題。其實(shí)看到這里有很多同學(xué)其實(shí)已經(jīng)有了解決問題的答案,那就是——加鎖。完全正確,那我們就馬上來探索一下python多線程中的鎖吧。
同樣我們先寫一段簡單的代碼進(jìn)行講解:
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模塊獲得了一個(gè)鎖lock,隨后在執(zhí)行操作賬戶余額的函數(shù)時(shí)加了鎖,這樣就保證了多個(gè)線程同一時(shí)刻只能有一個(gè)線程進(jìn)行賬戶余額的操作,這就保證了線程的安全,保證了共享變量不會出現(xiàn)我們意想不到的結(jié)果。是不是很簡單呢~
在你洋洋得意學(xué)會加鎖的時(shí)候,我們再來考慮一段代碼:
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ù),在這個(gè)函數(shù)中,我們將繼續(xù)調(diào)用unreentrancelock_callee()函數(shù)。隨后我們執(zhí)行這段程序,發(fā)現(xiàn)輸出是這樣的:
unreentrancelock_caller
Thread unreentrancelock_caller with lock
但是注意觀察,程序執(zhí)行結(jié)束了嗎?并沒有。這是為什么呢?這是因?yàn)槲覀冞@里獲得的lock是一個(gè)不可重入鎖,也就是自旋鎖,當(dāng)我們的程序執(zhí)行到unreentrancelock_callee()中請求lock的時(shí)候,我們發(fā)現(xiàn)我們其實(shí)已經(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,我們可以直接利用獲得到的可重入鎖進(jìn)行使用,執(zhí)行結(jié)果如下
reentrancelock_caller
Thread reentrancelock_caller with rlock
reentrancelock_callee
Thread reentrancelock_caller with rlock
End.
是不是感覺可重入鎖的使用也很方便呢~
2.3 python多線程之定時(shí)任務(wù)
在threading模塊中,博主自認(rèn)為還有一個(gè)比較實(shí)用的功能拿來分享一下,那就是Timer執(zhí)行定時(shí)任務(wù),比如我們當(dāng)前有一個(gè)任務(wù)我們不需要他馬上執(zhí)行,而是定時(shí)執(zhí)行,我們就可以用到它了~
import time
from threading import Timer
def timer_test():
print("共經(jīng)歷時(shí)間 %.2f 秒" % (time.time() - begin))
my_timer = Timer(5, timer_test)
begin = time.time()
my_timer.start()
在新建我們的my_timer實(shí)例時(shí),傳入的第一個(gè)參數(shù)為需要定時(shí)的時(shí)間,而第二個(gè)參數(shù)是我們要執(zhí)行的函數(shù)名。
輸出為
共經(jīng)歷時(shí)間 5.00 秒
2.4 python多線程總結(jié)
通過本文的介紹,相信你已經(jīng)對python多線程的知識有了一定的了解,正如我們所見,python多線程并不能像java多線程一樣同時(shí)運(yùn)行在CPU多個(gè)核心上,其運(yùn)用的場合主要為IO密集型程序。
那么也許你有這樣的問題,那面對計(jì)算密集型的程序時(shí)我們該怎么辦呢?我們是不是必須要使用java解決問題呢?答案是我們可以使用python多進(jìn)程,對于這一部分內(nèi)容,博主將在下一篇博文進(jìn)行討論。