python并發(fā)之一:一篇文章搞懂python多線程(理論+實(shí)踐)

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)行討論。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一. 操作系統(tǒng)概念 操作系統(tǒng)位于底層硬件與應(yīng)用軟件之間的一層.工作方式: 向下管理硬件,向上提供接口.操作系統(tǒng)進(jìn)行...
    月亮是我踢彎得閱讀 6,162評論 3 28
  • 線程 操作系統(tǒng)線程理論 線程概念的引入背景 進(jìn)程 之前我們已經(jīng)了解了操作系統(tǒng)中進(jìn)程的概念,程序并不能單獨(dú)運(yùn)行,只有...
    go以恒閱讀 1,795評論 0 6
  • 多進(jìn)程 要讓python程序?qū)崿F(xiàn)多進(jìn)程,我們先了解操作系統(tǒng)的相關(guān)知識。 Unix、Linux操作系統(tǒng)提供了一個(gè)fo...
    蓓蓓的萬能男友閱讀 675評論 0 1
  • 問題 本月圈外商學(xué)院課程-輔導(dǎo)下屬之認(rèn)識你的下屬中講到通過MBIT性格測試來認(rèn)識下屬。在1月-用冰山模型認(rèn)識自己,...
    日出晨安ppx閱讀 4,870評論 0 0
  • 看這肩上的云, 多么炫彩而遼闊! 讓人忍不住抓取這轉(zhuǎn)瞬即逝的一瞥! 雖然它終將失落在云霓的金輝隱逝的黃昏, 但卻是...
    素心如荷閱讀 337評論 0 0

友情鏈接更多精彩內(nèi)容