多線程編程技術(shù)可以實(shí)現(xiàn)代碼并行,優(yōu)化處理能力,同時(shí)可以將代碼劃分為功能更小的模塊直撤,使代碼的可重用性更好。
這里將介紹Python中的多線程編程蜕着。多線程一直是Python學(xué)習(xí)中的重點(diǎn)和難點(diǎn)谋竖,需要反復(fù)練習(xí)和研究红柱。
線程和進(jìn)程
在學(xué)習(xí)多線程的使用之前,需要先了解線程蓖乘、進(jìn)程锤悄、多線程的概念。
1 進(jìn)程
進(jìn)程(Process嘉抒,有時(shí)被稱為重量級(jí)進(jìn)程)是程序的一次執(zhí)行零聚。每個(gè)進(jìn)程都有自己的地址空間、內(nèi)存些侍、數(shù)據(jù)棧以及記錄運(yùn)行軌跡的輔助數(shù)據(jù)隶症,操作系統(tǒng)管理運(yùn)行的所有進(jìn)程,并為這些進(jìn)程公平分配時(shí)間岗宣。進(jìn)程可以通過(guò)fork和spawn操作完成其他任務(wù)蚂会。因?yàn)楦鱾€(gè)進(jìn)程有自己的內(nèi)存
空間、數(shù)據(jù)棧等耗式,所以只能使用進(jìn)程間通信(IPC)胁住,而不能直接共享信息。
2 線程
線程(Thread刊咳,有時(shí)被稱為輕量級(jí)進(jìn)程)跟進(jìn)程有些相似彪见,不同的是所有線程運(yùn)行在同一個(gè)進(jìn)程中,共享運(yùn)行環(huán)境娱挨。
線程有開(kāi)始余指、順序執(zhí)行和結(jié)束3部分,有一個(gè)自己的指令指針跷坝,記錄運(yùn)行到什么地方浪规。線程的運(yùn)行可能被搶占(中斷)或暫時(shí)被掛起(睡眠),從而讓其他線程運(yùn)行探孝,這叫作讓步。一個(gè)進(jìn)程中的各個(gè)線程之間共享同一片數(shù)據(jù)空間誉裆,所以線程之間可以比進(jìn)程之間更方便地共享數(shù)據(jù)和相互通信顿颅。
線程一般是并發(fā)執(zhí)行的。正是由于這種并行和數(shù)據(jù)共享的機(jī)制足丢,使得多個(gè)任務(wù)的合作變得可能粱腻。實(shí)際上,在單CPU系統(tǒng)中斩跌,真正的并發(fā)并不可能绍些,每個(gè)線程會(huì)被安排成每次只運(yùn)行一小會(huì)兒,然后就把CPU讓出來(lái)耀鸦,讓其他線程運(yùn)行柬批。
在進(jìn)程的整個(gè)運(yùn)行過(guò)程中啸澡,每個(gè)線程都只做自己的事,需要時(shí)再跟其他線程共享運(yùn)行結(jié)果氮帐。多個(gè)線程共同訪問(wèn)同一片數(shù)據(jù)不是完全沒(méi)有危險(xiǎn)的嗅虏,由于數(shù)據(jù)訪問(wèn)的順序不一樣,因此有可能導(dǎo)致數(shù)據(jù)結(jié)果不一致的問(wèn)題上沐,這叫作競(jìng)態(tài)條件皮服。大多數(shù)線程庫(kù)都帶有一系列同步原語(yǔ),用于控制線程的執(zhí)行和數(shù)據(jù)的訪問(wèn)参咙。
3 多線程與多進(jìn)程
對(duì)于“多任務(wù)”這個(gè)詞龄广,相信讀者不會(huì)第一次看見(jiàn),現(xiàn)在的操作系統(tǒng)(如Mac OS X蕴侧、UNIX择同、Linux、Windows等)都支持“多任務(wù)”操作系統(tǒng)戈盈。
什么叫“多任務(wù)”呢奠衔?簡(jiǎn)單地說(shuō),就是系統(tǒng)可以同時(shí)運(yùn)行多個(gè)任務(wù)塘娶。比如归斤,一邊用瀏覽器上網(wǎng),一邊聽(tīng)云音樂(lè)刁岸,一邊聊天脏里,這就是多任務(wù)。此時(shí)手頭已經(jīng)有3個(gè)任務(wù)在運(yùn)行了虹曙。如果查看任務(wù)管理器迫横,可以看到還有很多任務(wù)悄悄在后臺(tái)運(yùn)行著,只是桌面上沒(méi)有顯示而已酝碳。
對(duì)于操作系統(tǒng)來(lái)說(shuō)矾踱,一個(gè)任務(wù)就是一個(gè)進(jìn)程,開(kāi)啟多個(gè)任務(wù)就是多進(jìn)程疏哗。
有些進(jìn)程不止可以同時(shí)做一件事呛讲,比如Word可以同時(shí)打字、拼寫檢查返奉、打印等贝搁。在一個(gè)進(jìn)程內(nèi)部,要同時(shí)做多件事芽偏,就需要同時(shí)運(yùn)行多個(gè)線程雷逆。
多線程類似于同時(shí)執(zhí)行多個(gè)不同的程序,多線程運(yùn)行有以下3個(gè)優(yōu)點(diǎn):
(1)使用線程可以把占據(jù)長(zhǎng)時(shí)間的程序中的任務(wù)放到后臺(tái)去處理污尉。
(2)用戶界面可以更加吸引人膀哲,比如用戶單擊一個(gè)按鈕往产,用于觸發(fā)某些事件的處理,可以彈出一個(gè)進(jìn)度條顯示處理的進(jìn)度等太。
(3)程序的運(yùn)行速度可能加快捂齐。
在實(shí)現(xiàn)一些等待任務(wù)(如用戶輸入、文件讀寫和網(wǎng)絡(luò)收發(fā)數(shù)據(jù)等)時(shí)缩抡,使用多線程更加有用奠宜。在這種情況下,我們可以釋放一些珍貴資源(如內(nèi)存占用等)瞻想。
線程在執(zhí)行過(guò)程中與進(jìn)程還是有區(qū)別的压真。每個(gè)獨(dú)立線程有一個(gè)程序運(yùn)行的入口、順序執(zhí)行序列和程序的出口蘑险。但是線程不能獨(dú)立執(zhí)行滴肿,必須依存在進(jìn)程中,由進(jìn)程提供多個(gè)線程執(zhí)行控制佃迄。
由于每個(gè)進(jìn)程至少要干一件事泼差,因此一個(gè)進(jìn)程至少有一個(gè)線程。當(dāng)然呵俏,如Word這種復(fù)雜的進(jìn)程可以有多個(gè)線程堆缘,多個(gè)線程可以同時(shí)執(zhí)行。多線程的執(zhí)行方式和多進(jìn)程是一樣的普碎,也是由操作系統(tǒng)在多個(gè)線程之間快速切換吼肥,讓每個(gè)線程都短暫交替運(yùn)行,看起來(lái)就像同時(shí)執(zhí)行一樣麻车。當(dāng)然缀皱,真正同時(shí)執(zhí)行多線程需要多核CPU才能實(shí)現(xiàn)。
我們前面編寫的所有Python程序都是執(zhí)行單任務(wù)的進(jìn)程动猬,也就是只有一個(gè)線程啤斗。如果我們要同時(shí)執(zhí)行多個(gè)任務(wù),怎么辦呢赁咙?
有兩種解決方法:一種方法是啟動(dòng)多個(gè)進(jìn)程钮莲,每個(gè)進(jìn)程雖然只有一個(gè)線程,但多個(gè)進(jìn)程可以一起執(zhí)行多個(gè)任務(wù)序目。另一種方法是啟動(dòng)一個(gè)進(jìn)程,在一個(gè)進(jìn)程內(nèi)啟動(dòng)多個(gè)線程伯襟,這樣多個(gè)線程也可以一起執(zhí)行多個(gè)任務(wù)猿涨。
當(dāng)然,還有第3種方法姆怪,就是啟動(dòng)多個(gè)進(jìn)程叛赚,每個(gè)進(jìn)程再啟動(dòng)多個(gè)線程澡绩,這樣同時(shí)執(zhí)行的任務(wù)就更多了,不過(guò)這種模型過(guò)于復(fù)雜俺附,實(shí)際很少采用肥卡。
同時(shí)執(zhí)行多個(gè)任務(wù)時(shí),各個(gè)任務(wù)之間并不是沒(méi)有關(guān)聯(lián)的事镣,而是需要相互通信和協(xié)調(diào)步鉴,有時(shí)任務(wù)1必須暫停等待任務(wù)2完成后才能繼續(xù)執(zhí)行,有時(shí)任務(wù)3和任務(wù)4不能同時(shí)執(zhí)行璃哟。多進(jìn)程和多線程程序的復(fù)雜度遠(yuǎn)遠(yuǎn)高于我們前面寫的單進(jìn)程氛琢、單線程的程序。
不過(guò)很多時(shí)候随闪,沒(méi)有多任務(wù)還真不行阳似。想想在電腦上看電影,必須由一個(gè)線程播放視頻铐伴,另一個(gè)線程播放音頻撮奏,否則使用單線程實(shí)現(xiàn)只能先把視頻播放完再播放音頻,或者先把音頻播放完再播放視頻当宴,這樣顯然不行畜吊。
總而言之,多線程是多個(gè)相互關(guān)聯(lián)的線程的組合即供,多進(jìn)程是多個(gè)互相獨(dú)立的進(jìn)程的組合定拟。線程是最小的執(zhí)行單元,進(jìn)程至少由一個(gè)線程組成逗嫡。
使用線程
如何使用線程青自,線程中有哪些比較值得學(xué)習(xí)的模塊呢?本節(jié)將對(duì)線程的使用做概念性的講解驱证,下一節(jié)再給出一些具體示例以供參考延窜。
1 全局解釋器鎖
Python代碼的執(zhí)行由Python虛擬機(jī)(解釋器主循環(huán))控制。Python在設(shè)計(jì)之初就考慮到在主循環(huán)中只能有一個(gè)線程執(zhí)行抹锄,雖然Python解釋
器中可以“運(yùn)行”多個(gè)線程逆瑞,但是在任意時(shí)刻只有一個(gè)線程在解釋器中運(yùn)行。
Python虛擬機(jī)的訪問(wèn)由全局解釋器鎖(GIL)控制伙单,這個(gè)鎖能保證同一時(shí)刻只有一個(gè)線程運(yùn)行获高。
在多線程環(huán)境中,Python虛擬機(jī)按以下方式執(zhí)行:
(1)設(shè)置GIL吻育。
(2)切換到一個(gè)線程運(yùn)行念秧。
(3)運(yùn)行指定數(shù)量的字節(jié)碼指令或線程主動(dòng)讓出控制(可以調(diào)用time.sleep(0))。
(4)把線程設(shè)置為睡眠狀態(tài)布疼。
(5)解鎖GIL摊趾。
(6)再次重復(fù)以上所有步驟币狠。
在調(diào)用外部代碼(如C/C++擴(kuò)展函數(shù))時(shí),GIL將被鎖定砾层。直到這個(gè)函數(shù)結(jié)束為止(由于在此期間沒(méi)有運(yùn)行Python的字節(jié)碼漩绵,因此不會(huì)做線程切換),編寫擴(kuò)展的程序員可以主動(dòng)解鎖GIL肛炮。
2 退出線程
當(dāng)一個(gè)線程結(jié)束計(jì)算止吐,它就退出了。線程可以調(diào)用_thread.exit()等退出函數(shù)铸董,也可以使用Python退出進(jìn)程的標(biāo)準(zhǔn)方法(如sys.exit()或拋出一個(gè)SystemExit異常)祟印,不過(guò)不可以直接“殺掉”(kill)一個(gè)線程。
不建議使用_thread模塊粟害。很明顯的一個(gè)原因是蕴忆,當(dāng)主線程退出時(shí),其他線程如果沒(méi)有被清除就會(huì)退出悲幅。另一個(gè)模塊threading能確保所有“重要的”子線程都退出后套鹅,進(jìn)程才會(huì)結(jié)束。
3 Python的線程模塊
Python提供了幾個(gè)用于多線程編程的模塊汰具,包括_thread卓鹿、threading和Queue等。_thread和threading模塊允許程序員創(chuàng)建和管理線程留荔。_thread模塊提供了基本線程和鎖的支持吟孙,threading提供了更高級(jí)別、功能更強(qiáng)的線程管理功能聚蝶。Queue模塊允許用戶創(chuàng)建一個(gè)可以用于多個(gè)線程之間共享數(shù)據(jù)的隊(duì)列數(shù)據(jù)結(jié)構(gòu)杰妓。
避免使用_thread模塊,原因有3點(diǎn)碘勉。首先巷挥,更高級(jí)別的threading模塊更為先進(jìn),對(duì)線程的支持更為完善验靡,而且使用_thread模塊里的屬性有可能與threading沖突倍宾;其次,低級(jí)別的_thread模塊的同步原語(yǔ)很少(實(shí)際上只有一個(gè))胜嗓,而threading模塊有很多高职;再者,_thread模塊中在主線程結(jié)束時(shí)辞州,所有線程都會(huì)被強(qiáng)制結(jié)束怔锌,沒(méi)有警告也不會(huì)有正常清除工作,至少threading模塊能確保重要子線程退出后進(jìn)程才退出。
_thread
模塊
Python中調(diào)用_thread
模塊中的start_new_thread()
函數(shù)產(chǎn)生新線程产禾。_thread
的語(yǔ)法如下:
_thread.start_new_thread (function, args[, kwargs])
其中,function為線程函數(shù)牵啦;args為傳遞給線程函數(shù)的參數(shù)亚情,必須是tuple類型;kwargs為可選參數(shù)哈雏。
_thread
模塊除了產(chǎn)生線程外楞件,還提供基本同步數(shù)據(jù)結(jié)構(gòu)鎖對(duì)象(lock object,也叫原語(yǔ)鎖裳瘪、簡(jiǎn)單鎖土浸、互斥鎖、互斥量彭羹、二值信號(hào)量)黄伊。同步原語(yǔ)與線程管理是密不可分的。
我們看如下示例派殷。
#! /usr/bin/python
# -*-coding:UTF-8-*-
import _thread
from time import sleep
from datetime import datetime
date_time_format = '%y-%M-%d %H:%M:%S'
def date_time_str(date_time):
return datetime.strftime(date_time, date_time_format)
def loop_one():
print('+++線程一開(kāi)始于:', date_time_str(datetime.now()))
print('+++線程一休眠4 秒')
sleep(4)
print('+++線程一休眠結(jié)束还最,結(jié)束于:', date_time_str(datetime.now()))
def loop_two():
print('***線程二開(kāi)始時(shí)間:', date_time_str(datetime.now()))
print('***線程二休眠2 秒')
sleep(2)
print('***線程二休眠結(jié)束,結(jié)束時(shí)間:', date_time_str(datetime.now()))
def main():
print('------所有線程開(kāi)始時(shí)間:', date_time_str(datetime.now()))
_thread.start_new_thread(loop_one, ())
_thread.start_new_thread(loop_two, ())
sleep(6)
print('------所有線程結(jié)束時(shí)間:', date_time_str(datetime.now()))
if __name__ == '__main__':
main()
執(zhí)行結(jié)果如下:
------所有線程開(kāi)始時(shí)間: 16-44-06 21:44:05
+++線程一開(kāi)始于: 16-44-06 21:44:05
+++線程一休眠4 秒
***線程二開(kāi)始時(shí)間: 16-44-06 21:44:05
***線程二休眠2 秒
***線程二休眠結(jié)束毡惜,結(jié)束時(shí)間: 16-44-06 21:44:07
+++線程一休眠結(jié)束拓轻,結(jié)束于: 16-44-06 21:44:09
------所有線程結(jié)束時(shí)間: 16-44-06 21:44:11
_thread
模塊提供了簡(jiǎn)單的多線程機(jī)制,兩個(gè)循環(huán)并發(fā)執(zhí)行经伙,總的運(yùn)行時(shí)間為最慢的線程的運(yùn)行時(shí)間(主線程6s)扶叉,而不是所有線程的運(yùn)行時(shí)間之和。start_new_thread()
要求至少傳兩個(gè)參數(shù)帕膜,即使想要運(yùn)行的函數(shù)不要參數(shù)枣氧,也要傳一個(gè)空元組。
sleep(6)是讓主線程停下來(lái)泳叠。主線程一旦運(yùn)行結(jié)束作瞄,就關(guān)閉運(yùn)行著的其他兩個(gè)線程。這可能造成主線程過(guò)早或過(guò)晚退出危纫,這時(shí)就要使用線程鎖宗挥,主線程可認(rèn)在兩個(gè)子線程都退出后立即退出。
示例代碼如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import _thread
from time import sleep
from datetime import datetime
loops = [4, 2]
date_time_format = '%y-%M-%d %H:%M:%S'
def date_time_str(date_time):
return datetime.strftime(date_time, date_time_format)
def loop(n_loop, n_sec, lock):
print('線程(', n_loop, ')開(kāi)始執(zhí)行:',
date_time_str(datetime.now()), '种蝶,先休眠(', n_sec, ')秒')
sleep(n_sec)
print('線程(', n_loop, ')休眠結(jié)束契耿,結(jié)束于:', date_time_str(datetime.now()))
lock.release()
def main():
print('---所有線程開(kāi)始執(zhí)行...')
locks = []
n_loops = range(len(loops))
for i in n_loops:
lock = _thread.allocate_lock()
lock.acquire()
locks.append(lock)
for i in n_loops:
_thread.start_new_thread(loop, (i, loops[i], locks[i]))
for i in n_loops:
while locks[i].locked(): pass
print('---所有線程執(zhí)行結(jié)束:', date_time_str(datetime.now()))
if __name__ == '__main__':
main()
執(zhí)行結(jié)果如下:
---所有線程開(kāi)始執(zhí)行...
線程( 1 )開(kāi)始執(zhí)行: 16-44-06 21:44:11 ,先休眠( 2 )秒
線程( 0 )開(kāi)始執(zhí)行: 16-44-06 21:44:11 螃征,先休眠( 4 )秒
線程( 1 )休眠結(jié)束搪桂,結(jié)束于: 16-44-06 21:44:13
線程( 0 )休眠結(jié)束,結(jié)束于: 16-44-06 21:44:15
---所有線程執(zhí)行結(jié)束: 16-44-06 21:44:15
可以看到,以上代碼使用了線程鎖踢械。
threading
模塊
更高級(jí)別的threading
模塊不僅提供了Thread
類酗电,還提供了各種非常好用的同步機(jī)制。
_thread
模塊不支持守護(hù)線程内列,當(dāng)主線程退出時(shí)撵术,所有子線程無(wú)論是否在工作,都會(huì)被強(qiáng)行退出话瞧。threading模塊支持守護(hù)線程嫩与,守護(hù)線程一般是一個(gè)等待客戶請(qǐng)求的服務(wù)器,如果沒(méi)有客戶提出請(qǐng)求交排,就一直等著划滋。如果設(shè)定一個(gè)線程為守護(hù)線程,就表示這個(gè)線程不重要埃篓,在進(jìn)程退出時(shí)处坪,不用等待這個(gè)線程退出。如果主線程退出時(shí)不用等待子線程完成架专,就要設(shè)定這些線程的daemon
屬性稻薇,即在線程Thread.start()
開(kāi)始前,調(diào)用setDaemon()
函數(shù)設(shè)定線程的daemon標(biāo)志(Thread.setDaemon(True))胶征,表示這個(gè)線程“不重要”塞椎。如果一定要等待子線程執(zhí)行完成再退出主線程,就什么都不用做或顯式調(diào)用Thread.setDaemon(False)
以保證daemon標(biāo)志為False睛低,可以調(diào)用Thread.isDaemon()
函數(shù)判斷daemon標(biāo)志的值案狠。新的子線程會(huì)繼承父線程的daemon標(biāo)志,整個(gè)Python在所有非守護(hù)線程退出后才會(huì)結(jié)束钱雷,即進(jìn)程中沒(méi)有非守護(hù)線程存在時(shí)才結(jié)束骂铁。
threading
的Thread
類
Thread
有很多_thread
模塊里沒(méi)有的函數(shù),Thread
對(duì)象的函數(shù)很豐富罩抗。下面創(chuàng)建一個(gè)Thread
的實(shí)例拉庵,傳給它一個(gè)函數(shù)。示例如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import threading
from time import sleep
from datetime import datetime
loops = [4, 2]
date_time_format = '%y-%M-%d %H:%M:%S'
def date_time_str(date_time):
return datetime.strftime(date_time, date_time_format)
def loop(n_loop, n_sec):
print('線程(', n_loop, ')開(kāi)始執(zhí)行:',
date_time_str(datetime.now()), '套蒂,先休眠(', n_sec, ')秒')
sleep(n_sec)
print('線程(', n_loop, ')休眠結(jié)束钞支,結(jié)束于:', date_time_str(datetime.now()))
def main():
print('---所有線程開(kāi)始執(zhí)行:', date_time_str(datetime.now()))
threads = []
n_loops = range(len(loops))
for i in n_loops:
t = threading.Thread(target=loop, args=(i, loops[i]))
threads.append(t)
for i in n_loops: # start threads
threads[i].start()
for i in n_loops: # wait for all
threads[i].join() # threads to finish
print('---所有線程執(zhí)行結(jié)束于:', date_time_str(datetime.now()))
if __name__ == '__main__':
main()
執(zhí)行結(jié)果如下:
---所有線程開(kāi)始執(zhí)行: 16-44-06 21:44:15
線程( 0 )開(kāi)始執(zhí)行: 16-44-06 21:44:15 ,先休眠( 4 )秒
線程( 1 )開(kāi)始執(zhí)行: 16-44-06 21:44:15 操刀,先休眠( 2 )秒
線程( 1 )休眠結(jié)束烁挟,結(jié)束于: 16-44-06 21:44:17
線程( 0 )休眠結(jié)束,結(jié)束于: 16-44-06 21:44:19
---所有線程執(zhí)行結(jié)束于: 16-44-06 21:44:19
由執(zhí)行結(jié)果我們看到骨坑,實(shí)例化一個(gè)Thread(調(diào)用Thread())與調(diào)用_thread.start_new_thread()最大的區(qū)別是新的線程不會(huì)立即開(kāi)始撼嗓。創(chuàng)建線程對(duì)象卻不想馬上開(kāi)始運(yùn)行線程時(shí),Thread
是一個(gè)很有用的同步特性。所有線程都創(chuàng)建之后且警,再一起調(diào)用start()
函數(shù)啟動(dòng)粉捻,而不是每創(chuàng)建一個(gè)線程就啟動(dòng)。而且不用管理一堆鎖的狀態(tài)(分配鎖斑芜、獲得鎖杀迹、釋放鎖、檢查鎖的等狀態(tài))押搪,只要簡(jiǎn)單對(duì)每個(gè)線程調(diào)用join()
主線程,等待子線程結(jié)束即可浅碾。join()
還可以設(shè)置timeout
參數(shù)大州,即主線程的超時(shí)時(shí)間。
join()
的另一個(gè)比較重要的方面是可以完全不用調(diào)用垂谢。一旦線程啟動(dòng)厦画,就會(huì)一直運(yùn)行,直到線程的函數(shù)結(jié)束并退出為止滥朱。如果主線程除了等線程結(jié)束外根暑,還有其他事情要做,就不用調(diào)用join()
徙邻,只有在等待線程結(jié)束時(shí)才調(diào)用排嫌。
我們?cè)倏词纠瑒?chuàng)建一個(gè)Thread
的實(shí)例缰犁,并傳給它一個(gè)可調(diào)用的類對(duì)象淳地。代碼如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import threading
from time import sleep
from datetime import datetime
loops = [4, 2]
date_time_format = '%y-%M-%d %H:%M:%S'
class ThreadFunc(object):
def __init__(self, func, args, name=''):
self.name = name
self.func = func
self.args = args
def __call__(self):
self.func(*self.args)
def date_time_str(date_time):
return datetime.strftime(date_time, date_time_format)
def loop(n_loop, n_sec):
print('線程(', n_loop, ')開(kāi)始執(zhí)行:',
date_time_str(datetime.now()), ',先休眠(', n_sec, ')秒')
sleep(n_sec)
print('線程(', n_loop, ')休眠結(jié)束帅容,結(jié)束于:', date_time_str(datetime.now()))
def main():
print('---所有線程開(kāi)始執(zhí)行:', date_time_str(datetime.now()))
threads = []
nloops = range(len(loops))
for i in nloops: # create all threads
t = threading.Thread(
target=ThreadFunc(loop, (i, loops[i]), loop.__name__))
threads.append(t)
for i in nloops: # start all threads
threads[i].start()
for i in nloops: # wait for completion
threads[i].join()
print('---所有線程執(zhí)行結(jié)束于:', date_time_str(datetime.now()))
if __name__ == '__main__':
main()
執(zhí)行結(jié)果如下:
---所有線程開(kāi)始執(zhí)行: 16-03-06 22:03:18
線程( 0 )開(kāi)始執(zhí)行: 16-03-06 22:03:18 颇象,先休眠( 4 )秒
線程( 1 )開(kāi)始執(zhí)行: 16-03-06 22:03:18 ,先休眠( 2 )秒
線程( 1 )休眠結(jié)束并徘,結(jié)束于: 16-03-06 22:03:20
線程( 0 )休眠結(jié)束遣钳,結(jié)束于: 16-03-06 22:03:22
---所有線程執(zhí)行結(jié)束于: 16-03-06 22:03:22
由執(zhí)行結(jié)果看到,與傳一個(gè)函數(shù)很相似的一個(gè)方法是麦乞,在創(chuàng)建線程時(shí)蕴茴,傳一個(gè)可調(diào)用的類的實(shí)例供線程啟動(dòng)時(shí)執(zhí)行,這是多線程編程的一個(gè)面向?qū)ο蟮姆椒ń阒薄O鄬?duì)于一個(gè)或幾個(gè)函數(shù)來(lái)說(shuō)荐开,類對(duì)象可以使用類的強(qiáng)大功能。創(chuàng)建新線程時(shí)简肴,Thread
對(duì)象會(huì)調(diào)用ThreadFunc
對(duì)象晃听,這時(shí)會(huì)用到一個(gè)特殊函數(shù)__call__()
。由于已經(jīng)有了要用的參數(shù),因此不用再傳到Thread()的構(gòu)造函數(shù)中能扒。對(duì)于有一個(gè)參數(shù)的元組佣渴,要使用self.func(*self.args)
方法。
從Thread
派生一個(gè)子類初斑,創(chuàng)建這個(gè)子類的實(shí)例辛润。從上面的代碼派生的代碼如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import threading
from time import sleep
from datetime import datetime
loops = [4, 2]
date_time_format = '%y-%M-%d %H:%M:%S'
class MyThread(threading.Thread):
def __init__(self, func, args, name=''):
threading.Thread.__init__(self)
self.name = name
self.func = func
self.args = args
def getResult(self):
return self.res
def run(self):
print('starting', self.name, 'at:', date_time_str(datetime.now()))
self.res = self.func(*self.args)
print(self.name, 'finished at:', date_time_str(datetime.now()))
def date_time_str(date_time):
return datetime.strftime(date_time, date_time_format)
def loop(n_loop, n_sec):
print('線程(', n_loop, ')開(kāi)始執(zhí)行:',
date_time_str(datetime.now()), ',先休眠(', n_sec, ')秒')
sleep(n_sec)
print('線程(', n_loop, ')休眠結(jié)束见秤,結(jié)束于:', date_time_str(datetime.now()))
def main():
print('---所有線程開(kāi)始執(zhí)行:', date_time_str(datetime.now()))
threads = []
n_loops = range(len(loops))
for i in n_loops:
t = MyThread(loop, (i, loops[i]),
loop.__name__)
threads.append(t)
for i in n_loops:
threads[i].start()
for i in n_loops:
threads[i].join()
print('---所有線程執(zhí)行結(jié)束于:', date_time_str(datetime.now()))
if __name__ == '__main__':
main()
執(zhí)行結(jié)果如下:
---所有線程開(kāi)始執(zhí)行: 16-22-06 22:22:20
starting loop at: 16-22-06 22:22:20
線程( 0 )開(kāi)始執(zhí)行: 16-22-06 22:22:20 砂竖,先休眠( 4 )秒
starting loop at: 16-22-06 22:22:20
線程( 1 )開(kāi)始執(zhí)行: 16-22-06 22:22:20 ,先休眠( 2 )秒
線程( 1 )休眠結(jié)束鹃答,結(jié)束于: 16-22-06 22:22:22
loop finished at: 16-22-06 22:22:22
線程( 0 )休眠結(jié)束乎澄,結(jié)束于: 16-22-06 22:22:24
loop finished at: 16-22-06 22:22:24
---所有線程執(zhí)行結(jié)束于: 16-22-06 22:22:24
由代碼片段和執(zhí)行結(jié)果我們看到,子類化Thread
類测摔,MyThread
子類的構(gòu)造函數(shù)一定要先調(diào)用基類的構(gòu)造函數(shù)置济,特殊函數(shù)__call__()
在子類中,名字要改為run()
锋八。在MyThread
類中浙于,加入一些用于調(diào)試的輸出信息,把代碼保存到MyThread
模塊中挟纱,并導(dǎo)入這個(gè)類羞酗。使用self.func()
函數(shù)運(yùn)行這些函數(shù),并把結(jié)果保存到實(shí)現(xiàn)的self.res
屬性中紊服,創(chuàng)建一個(gè)新函數(shù)getResult()
得到結(jié)果整慎。
線程同步
如果多個(gè)線程共同修改某個(gè)數(shù)據(jù),就可能出現(xiàn)不可預(yù)料的結(jié)果围苫。為了保證數(shù)據(jù)的正確性裤园,需要對(duì)多個(gè)線程進(jìn)行同步。
使用Thread
對(duì)象的Lock
和RLock
可以實(shí)現(xiàn)簡(jiǎn)單的線程同步剂府,這兩個(gè)對(duì)象都有acquire
方法和release
方法拧揽。對(duì)于每次只允許一個(gè)線程操作的數(shù)據(jù),可以將操作放到acquire
和release
方法之間腺占。
多線程的優(yōu)勢(shì)在于可以同時(shí)運(yùn)行多個(gè)任務(wù)淤袜,但當(dāng)線程需要共享數(shù)據(jù)時(shí),可能存在數(shù)據(jù)不同步的問(wèn)題衰伯。
考慮這樣一種情況:一個(gè)列表里所有元素都是0铡羡,線程set
從后向前把所有元素改成1,而線程print
負(fù)責(zé)從前往后讀取列表并輸出意鲸。
線程set開(kāi)始改的時(shí)候烦周,線程print可能就來(lái)輸出列表了尽爆,輸出就成了一半0一半1,這就是數(shù)據(jù)不同步的問(wèn)題读慎。為了避免這種情況漱贱,引入了鎖的概念。
鎖有兩種狀態(tài)——鎖定和未鎖定夭委。當(dāng)一個(gè)線程(如set)要訪問(wèn)共享數(shù)據(jù)時(shí)幅狮,必須先獲得鎖定;如果已經(jīng)有別的線程(如print)獲得鎖定了株灸,就讓線程set暫停崇摄,也就是同步阻塞;等到線程print訪問(wèn)完畢慌烧,釋放鎖以后逐抑,再讓線程set繼續(xù)。
經(jīng)過(guò)這樣的處理杏死,輸出列表時(shí)要么全部輸出0,要么全部輸出1捆交,不會(huì)再出現(xiàn)一半0一半1的尷尬場(chǎng)面淑翼。
示例代碼如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import threading
from time import sleep
from datetime import datetime
date_time_format = '%y-%M-%d %H:%M:%S'
class MyThread (threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print ("開(kāi)啟線程: " + self.name)
# 獲取鎖,用于線程同步
threadLock.acquire()
print_time(self.name, self.counter, 3)
# 釋放鎖品追,開(kāi)啟下一個(gè)線程
threadLock.release()
def date_time_str(date_time):
return datetime.strftime(date_time, date_time_format)
def print_time(threadName, delay, counter):
while counter:
sleep(delay)
print ("%s: %s" % (threadName, date_time_str(datetime.now())))
counter -= 1
def main():
# 創(chuàng)建新線程
thread1 = MyThread(1, "Thread-1", 1)
thread2 = MyThread(2, "Thread-2", 2)
# 開(kāi)啟新線程
thread1.start()
thread2.start()
# 添加線程到線程列表
threads.append(thread1)
threads.append(thread2)
# 等待所有線程完成
for t in threads:
t.join()
print("退出主線程")
if __name__ == "__main__":
threadLock = threading.Lock()
threads = []
main()
執(zhí)行結(jié)果如下:
開(kāi)啟線程: Thread-1
開(kāi)啟線程: Thread-2
Thread-1: 16-15-06 23:15:25
Thread-1: 16-15-06 23:15:26
Thread-1: 16-15-06 23:15:27
Thread-2: 16-15-06 23:15:29
Thread-2: 16-15-06 23:15:31
Thread-2: 16-15-06 23:15:33
退出主線程
由執(zhí)行結(jié)果看到玄括,程序正確得到了同步效果。
線程優(yōu)先級(jí)隊(duì)列
Queue模塊可以用來(lái)進(jìn)行線程間的通信肉瓦,讓各個(gè)線程之間共享數(shù)據(jù)遭京。
Python的Queue模塊提供了同步、線程安全的隊(duì)列類泞莉,包括FIFO(先入先出)隊(duì)列Queue哪雕、LIFO(后入先出)隊(duì)列LifoQueue和優(yōu)先級(jí)隊(duì)列PriorityQueue。這些隊(duì)列都實(shí)現(xiàn)了鎖原語(yǔ)鲫趁,能夠在多線程中直接使用斯嚎。可以使用隊(duì)列實(shí)現(xiàn)線程間的同步挨厚。
Queue模塊中的常用方法如表所示堡僻。下面通過(guò)以下示例了解其中一些方法的使用。
#! /usr/bin/python
# -*-coding:UTF-8-*-
import threading
import queue
from time import sleep
class MyThread (threading.Thread):
def __init__(self, threadID, name, q):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.q = q
def run(self):
print ("開(kāi)啟線程:" + self.name)
process_data(self.name, self.q)
print ("退出線程:" + self.name)
def process_data(threadName, q):
while not exitFlag:
queueLock.acquire()
if not workQueue.empty():
data = q.get()
queueLock.release()
print ("%s processing %s" % (threadName, data))
else:
queueLock.release()
sleep(1)
def main():
global exitFlag
exitFlag = 0
threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
threads = []
threadID = 1
# 創(chuàng)建新線程
for tName in threadList:
thread = MyThread(threadID, tName, workQueue)
thread.start()
threads.append(thread)
threadID += 1
# 填充隊(duì)列
queueLock.acquire()
for word in nameList:
workQueue.put(word)
queueLock.release()
# 等待隊(duì)列清空
while not workQueue.empty():
pass
# 通知線程是退出的時(shí)候了
exitFlag = 1
# 等待所有線程完成
for t in threads:
t.join()
print ("退出主線程")
if __name__ == "__main__":
queueLock = threading.Lock()
workQueue = queue.Queue(10)
main()
執(zhí)行結(jié)果如下:
開(kāi)啟線程:Thread-1
開(kāi)啟線程:Thread-2
開(kāi)啟線程:Thread-3
Thread-3 processing One
Thread-2 processing Two
Thread-1 processing Three
Thread-3 processing Four
Thread-1 processing Five
退出線程:Thread-3
退出線程:Thread-1
退出線程:Thread-2
退出主線程
線程與進(jìn)程比較
多進(jìn)程和多線程是實(shí)現(xiàn)多任務(wù)最常用的兩種方式疫剃。下面通過(guò)線程切換钉疫、計(jì)算密集情況和異步性能3方面討論一下這兩種方式的優(yōu)缺點(diǎn)。
首先巢价,要實(shí)現(xiàn)多任務(wù)牲阁,我們通常會(huì)設(shè)計(jì)Master-Worker模式固阁,Master負(fù)責(zé)分配任務(wù),Worker負(fù)責(zé)執(zhí)行任務(wù)咨油。因此您炉,在多任務(wù)環(huán)境下,通常是一個(gè)Master役电、多個(gè)Worker赚爵。
如果用多進(jìn)程實(shí)現(xiàn)Master-Worker,主進(jìn)程就是Master法瑟,其他進(jìn)程就是Worker冀膝。
如果用多線程實(shí)現(xiàn)Master-Worker,主線程就是Master霎挟,其他線程就是Worker窝剖。
多進(jìn)程模式最大的優(yōu)點(diǎn)是穩(wěn)定性高,因?yàn)橐粋€(gè)子進(jìn)程崩潰不會(huì)影響主進(jìn)程和其他子進(jìn)程(當(dāng)然酥夭,主進(jìn)程掛了所有進(jìn)程就全掛了赐纱,但是Master進(jìn)程只負(fù)責(zé)分配任務(wù),掛掉的概率低)熬北。著名的Apache最早就采用多進(jìn)程模式疙描。
多進(jìn)程模式的缺點(diǎn)是創(chuàng)建進(jìn)程的代價(jià)大。在UNIX/Linux系統(tǒng)下用fork調(diào)用還行讶隐,在Windows下創(chuàng)建進(jìn)程開(kāi)銷非常大起胰。另外,操作系統(tǒng)能同時(shí)運(yùn)行的進(jìn)程數(shù)有限巫延,在內(nèi)存和CPU的限制下效五,如果幾千個(gè)進(jìn)程同時(shí)運(yùn)行,操作系統(tǒng)就連調(diào)度都會(huì)出問(wèn)題炉峰。
多線程模式通常比多進(jìn)程快一點(diǎn)畏妖,但是也快不了多少。多線程模式致命的缺點(diǎn)是任何一個(gè)線程掛掉都可能直接造成整個(gè)進(jìn)程崩潰疼阔,因?yàn)樗?br>
有線程共享進(jìn)程的內(nèi)存瓜客。在Windows中,如果一個(gè)線程執(zhí)行的代碼出了問(wèn)題竿开,就可以看到這樣的提示:“該程序執(zhí)行了非法操作谱仪,即將關(guān)閉”,其實(shí)往往是某個(gè)線程出了問(wèn)題否彩,但是操作系統(tǒng)會(huì)強(qiáng)制結(jié)束整個(gè)進(jìn)程疯攒。
在Windows中,多線程的效率比多進(jìn)程高列荔,所以微軟的IIS服務(wù)器默認(rèn)采用多線程模式敬尺。由于多線程存在穩(wěn)定性的問(wèn)題枚尼,因此IIS的穩(wěn)定性不如Apache。為了緩解這個(gè)問(wèn)題砂吞,IIS和Apache有了多進(jìn)程+多線程的混合模式署恍,問(wèn)題越來(lái)越復(fù)雜。
1 線程切換
無(wú)論是多進(jìn)程還是多線程蜻直,數(shù)量太多盯质,效率肯定上不去。
我們打個(gè)比方概而,你正在準(zhǔn)備中考呼巷,每天晚上需要做語(yǔ)文、數(shù)學(xué)赎瑰、英語(yǔ)王悍、物理、化學(xué)5科作業(yè)餐曼,每科作業(yè)耗時(shí)1小時(shí)油挥。
如果你先花1小時(shí)做語(yǔ)文作業(yè)覆山,做完后再花1小時(shí)做數(shù)學(xué)作業(yè)坎炼,這樣依次全部做完粉寞,一共花5小時(shí)沽讹,這種方式稱為單任務(wù)模型或批處理任務(wù)模型吗跋。
如果你打算切換到多任務(wù)模型逢艘,可以先做1分鐘語(yǔ)文蒸绩,切換到數(shù)學(xué)作業(yè)做1分鐘鳞青,再切換到英語(yǔ)霸饲,以此類推,只要切換速度足夠快臂拓,這種方式就和單核CPU執(zhí)行多任務(wù)一樣了厚脉。以幼兒園小朋友的眼光來(lái)看,你就正在同時(shí)寫5科作業(yè)胶惰。
不過(guò)切換作業(yè)是有代價(jià)的傻工,比如從語(yǔ)文切換到數(shù)學(xué),要先收拾桌子上的語(yǔ)文書(shū)本孵滞、鋼筆(保存現(xiàn)場(chǎng))中捆,然后打開(kāi)數(shù)學(xué)課本,找出圓規(guī)直尺(準(zhǔn)備新環(huán)境)坊饶,才能開(kāi)始做數(shù)學(xué)作業(yè)泄伪。操作系統(tǒng)在切換進(jìn)程或線程時(shí)也一樣,需要先保存當(dāng)前執(zhí)行的現(xiàn)場(chǎng)環(huán)境(CPU寄存器狀態(tài)匿级、內(nèi)存頁(yè)等)蟋滴,然后把新任務(wù)的執(zhí)行環(huán)境準(zhǔn)備好(恢復(fù)上次的寄存器狀態(tài)染厅,切換內(nèi)存頁(yè)等),才能開(kāi)始執(zhí)行津函。這個(gè)切換過(guò)程雖然很快肖粮,但是也需要耗費(fèi)時(shí)間。如果有幾千個(gè)任務(wù)同時(shí)進(jìn)行尔苦,操作系統(tǒng)可能主要忙著切換任務(wù)涩馆,根本沒(méi)有多少時(shí)間執(zhí)行任務(wù)。這種情況最常見(jiàn)的就是硬盤狂響蕉堰、點(diǎn)窗口無(wú)反應(yīng)凌净,這時(shí)系統(tǒng)處于假死狀態(tài)。
所以屋讶,多任務(wù)一旦多到一個(gè)限度冰寻,就會(huì)消耗系統(tǒng)所有資源,導(dǎo)致效率急劇下降皿渗,所有任務(wù)都做不好斩芭。
2 計(jì)算密集型與IO密集型
是否采用多任務(wù)的第二個(gè)考慮是任務(wù)類型。我們可以把任務(wù)分為計(jì)算密集型和IO密集型乐疆。
計(jì)算密集型任務(wù)的特點(diǎn)是要進(jìn)行大量計(jì)算划乖,消耗CPU資源,如計(jì)算圓周率挤土、對(duì)視頻進(jìn)行高清解碼等琴庵,全靠CPU的運(yùn)算能力。計(jì)算密集型任務(wù)雖然可以用多任務(wù)完成仰美,但是任務(wù)越多迷殿,花在任務(wù)切換的時(shí)間就越多,CPU執(zhí)行任務(wù)的效率就越低咖杂。要最高效地利用CPU庆寺,計(jì)算密集型任務(wù)同時(shí)進(jìn)行的數(shù)量應(yīng)當(dāng)?shù)扔贑PU的核心數(shù)。
由于計(jì)算密集型任務(wù)時(shí)主要消耗CPU資源诉字,因此代碼運(yùn)行效率至關(guān)重要懦尝。Python腳本語(yǔ)言運(yùn)行效率很低,完全不適合計(jì)算密集型任務(wù)壤圃。計(jì)算密集型任務(wù)最好用C語(yǔ)言編寫陵霉。
涉及網(wǎng)絡(luò)、磁盤IO的任務(wù)都是IO密集型任務(wù)伍绳,這類任務(wù)的特點(diǎn)是CPU消耗很少踊挠,任務(wù)的大部分時(shí)間都在等待IO操作完成(因?yàn)镮O的速度遠(yuǎn)遠(yuǎn)低于CPU和內(nèi)存的速度)。IO密集型任務(wù)的任務(wù)越多墨叛,CPU效率越高止毕,不過(guò)有一個(gè)限度模蜡。大部分任務(wù)都是IO密集型任務(wù),如Web應(yīng)用扁凛。
IO密集型任務(wù)執(zhí)行期間忍疾,99%的時(shí)間都花在IO上,花在CPU上的時(shí)間很少谨朝,因此用運(yùn)行速度極快的C語(yǔ)言替換Python這樣運(yùn)行速度極低的腳本語(yǔ)言完全無(wú)法提升運(yùn)行效率卤妒。對(duì)于IO密集型任務(wù)而言,最適合的語(yǔ)言是開(kāi)發(fā)效率高(代碼量最少)的語(yǔ)言字币,腳本語(yǔ)言是首選则披,C語(yǔ)言最差。
3 異步IO
考慮到CPU和IO之間速度差異很大洗出,一個(gè)任務(wù)在執(zhí)行的過(guò)程中大部分時(shí)間都在等待IO操作士复,單進(jìn)程單線程模型會(huì)導(dǎo)致別的任務(wù)無(wú)法并行執(zhí)行,因此需要多進(jìn)程模型或多線程模型支持多任務(wù)并發(fā)執(zhí)行翩活。
現(xiàn)在的操作系統(tǒng)對(duì)IO操作已經(jīng)做了很大改進(jìn)阱洪,最大的特點(diǎn)是支持異步IO。如果充分利用操作系統(tǒng)提供的異步IO支持菠镇,就可以用單進(jìn)程單線程模型執(zhí)行多任務(wù)冗荸,這種全新模型稱為事件驅(qū)動(dòng)模型。Nginx就是支持異步IO的Web服務(wù)器利耍,在單核CPU上采用單進(jìn)程模型就可以高效支持多任務(wù)蚌本;在多核CPU上可以運(yùn)行多個(gè)進(jìn)程(數(shù)量與CPU核心數(shù)相同),充分利用多核CPU隘梨。由于系統(tǒng)總的進(jìn)程數(shù)量十分有限程癌,因此操作系統(tǒng)調(diào)度非常高效。用異步IO編程模型實(shí)現(xiàn)多任務(wù)是主要趨勢(shì)出嘹。
對(duì)應(yīng)到Python語(yǔ)言席楚,單進(jìn)程的異步編程模型稱為協(xié)程咬崔。有了協(xié)程的支持税稼,可以基于事件驅(qū)動(dòng)編寫高效的多任務(wù)程序。