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

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)容,博主將在下一篇博文進行討論慧域。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鲤竹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子昔榴,更是在濱河造成了極大的恐慌辛藻,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件论泛,死亡現(xiàn)場離奇詭異揩尸,居然都是意外死亡,警方通過查閱死者的電腦和手機屁奏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門岩榆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人坟瓢,你說我怎么就攤上這事勇边。” “怎么了折联?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵粒褒,是天一觀的道長。 經(jīng)常有香客問我诚镰,道長奕坟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任清笨,我火速辦了婚禮月杉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘抠艾。我一直安慰自己苛萎,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著腌歉,像睡著了一般蛙酪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上翘盖,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天桂塞,我揣著相機與錄音,去河邊找鬼馍驯。 笑死藐俺,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的泥彤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼卿啡,長吁一口氣:“原來是場噩夢啊……” “哼吟吝!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起颈娜,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤剑逃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后官辽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛹磺,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年同仆,在試婚紗的時候發(fā)現(xiàn)自己被綠了萤捆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡俗批,死狀恐怖俗或,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情岁忘,我是刑警寧澤辛慰,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站干像,受9級特大地震影響帅腌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜麻汰,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一速客、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧什乙,春花似錦挽封、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽智亮。三九已至,卻和暖如春点待,著一層夾襖步出監(jiān)牢的瞬間阔蛉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工癞埠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留状原,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓苗踪,卻偏偏與公主長得像颠区,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子通铲,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容

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