Python大學(xué)教程(第2版) 課件 第14章利用Python 進(jìn)行多任務(wù)編程_第1頁
Python大學(xué)教程(第2版) 課件 第14章利用Python 進(jìn)行多任務(wù)編程_第2頁
Python大學(xué)教程(第2版) 課件 第14章利用Python 進(jìn)行多任務(wù)編程_第3頁
Python大學(xué)教程(第2版) 課件 第14章利用Python 進(jìn)行多任務(wù)編程_第4頁
Python大學(xué)教程(第2版) 課件 第14章利用Python 進(jìn)行多任務(wù)編程_第5頁
已閱讀5頁,還剩36頁未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡介

第14章

利用Python進(jìn)行多任務(wù)編程

目錄2進(jìn)程和線程Python中的多線程編程Python中的進(jìn)程編程14.114.214.314.1進(jìn)程和線程簡介在計(jì)算機(jī)科學(xué)的背景下,多任務(wù)通常是指可以在同一時(shí)刻運(yùn)行多個(gè)應(yīng)用程序,而這里的每個(gè)應(yīng)用程序被稱為一個(gè)任務(wù)。多任務(wù)一方面是操作系統(tǒng)管理同時(shí)運(yùn)行的不同程序的一種方式,另一方面也可以被用來最大程度地利用硬件計(jì)算資源(例如,N核處理器中,每個(gè)核心都可以完成一個(gè)任務(wù),并且同時(shí)進(jìn)行,容易知道在極其理想的情況下計(jì)算效率可以提升N倍)。后者在當(dāng)下大數(shù)據(jù)處理和分析需求以及硬件計(jì)算能力快速提升的雙重背景下顯得尤為重要。讀者可能會(huì)經(jīng)常聽到并行處理、分布式計(jì)算等概念,以及流行的MapReduce、CUDA等并行計(jì)算平臺(tái),這些概念均是多任務(wù)協(xié)同計(jì)算的嘗試。在本節(jié)中,將首先幫助讀者熟悉多任務(wù)編程中的兩個(gè)重要概念—進(jìn)程和線程。由于深入剖析進(jìn)程和線程的內(nèi)部機(jī)制需要諸多先修知識(shí),下面僅盡可能通俗而簡要地從一個(gè)程序開發(fā)者的角度介紹這兩個(gè)概念,詳細(xì)知識(shí)可參閱相關(guān)教材。14.1.1進(jìn)程通俗地說,一個(gè)程序的一次獨(dú)立運(yùn)行是一個(gè)進(jìn)程,進(jìn)程是操作系統(tǒng)資源管理的最小單位。操作系統(tǒng)會(huì)隔離每個(gè)進(jìn)程,使程序員以及用戶可以認(rèn)為每個(gè)任務(wù)都是在獨(dú)占系統(tǒng)資源,包括內(nèi)存、處理器等。然而,從實(shí)現(xiàn)上來看,處理器上只可以同時(shí)運(yùn)行有限多的進(jìn)程。于是,操作系統(tǒng)采用進(jìn)程調(diào)度的方式,多個(gè)進(jìn)程需要排隊(duì)利用處理器的運(yùn)算資源。當(dāng)進(jìn)程得到處理器資源后,很短時(shí)間內(nèi)就會(huì)主動(dòng)或被動(dòng)交出處理器,讓給下一個(gè)進(jìn)程使用,而自己繼續(xù)排隊(duì)等待下一次使用機(jī)會(huì),這一過程被稱為進(jìn)程切換。由于輪轉(zhuǎn)時(shí)間很短,故從宏觀的視角看來就像是每個(gè)程序在連續(xù)不斷地運(yùn)行著。需要注意的是,這種進(jìn)程級(jí)別的輪轉(zhuǎn)調(diào)度需要付出較大時(shí)間代價(jià),即進(jìn)程切換所需要的時(shí)間是比較長的。另一方面,這種獨(dú)占性也意味著進(jìn)程之間無法直接共享數(shù)據(jù),而需要使用進(jìn)程間通信,或者其他基于前面章節(jié)所介紹的文件系統(tǒng)或數(shù)據(jù)庫系統(tǒng)的方法。14.1.2線程一個(gè)進(jìn)程是程序執(zhí)行的最小單位,每個(gè)處理器的核心上都可以運(yùn)行一個(gè)線程。通常,一個(gè)進(jìn)程中可能包含一個(gè)主線程。但為了提高效率,有些程序會(huì)使用多線程技術(shù),這時(shí)一個(gè)進(jìn)程中就會(huì)包含多個(gè)線程。例如,在一個(gè)手機(jī)聽歌軟件中,可能有一個(gè)主線程負(fù)責(zé)界面的刷新,有一個(gè)線程負(fù)責(zé)從互聯(lián)網(wǎng)上下載并緩存歌曲。在這個(gè)場景下多線程還是很有意義的,因?yàn)槿绻缑嫠⑿潞途W(wǎng)絡(luò)連接在同一線程中,很有可能界面的刷新操作會(huì)因網(wǎng)絡(luò)的延遲而等待,從而造成界面卡頓,會(huì)極大地影響用戶體驗(yàn);在下一小節(jié)中明確了串行和并行的概念之后,讀者將會(huì)對(duì)此有更深的理解。與進(jìn)程一樣,線程的數(shù)目也是遠(yuǎn)多于計(jì)算資源(處理器的核數(shù))的,操作系統(tǒng)會(huì)使用類似的調(diào)度策略。然而,由于同一進(jìn)程的多個(gè)線程之間共享內(nèi)存空間,線程間的通信會(huì)十分容易(甚至于不會(huì)被發(fā)覺)地實(shí)現(xiàn);同一進(jìn)程內(nèi)線程間的切換代價(jià)要遠(yuǎn)小于進(jìn)程的切換代價(jià),因?yàn)樵谡{(diào)度過程中只有少量的線程所獨(dú)有的數(shù)據(jù)需要切換。因此,線程有時(shí)也被稱為“輕量級(jí)進(jìn)程”。14.1.3串行,并發(fā)與并行了解串行、并發(fā)和并行的概念是理解多任務(wù)編程的基礎(chǔ)。串行是指程序中的每行指令按照其順序被計(jì)算機(jī)執(zhí)行,如之前章節(jié)中的程序均是串行執(zhí)行的。并發(fā),顧名思義,則是指同時(shí)運(yùn)行兩個(gè)程序,而這兩個(gè)程序是并列關(guān)系,沒有邏輯上的先后關(guān)系。并行,則是指并發(fā)的多個(gè)程序在同一時(shí)刻可以同時(shí)執(zhí)行其各自的指令。例如,有以下兩個(gè)程序funcA和funcB:deffuncA():foriin['A','B','C']:print(i,end='')deffuncB():foriin[1,2,3]:print(i,end='')當(dāng)調(diào)用函數(shù)funcA時(shí),程序會(huì)依次輸出'A'、'B'、'C'三個(gè)字符,這個(gè)順序是在函數(shù)中預(yù)先指定好的(即字符在list中出現(xiàn)的順序),而函數(shù)的每次執(zhí)行都會(huì)按照預(yù)先定義順序地執(zhí)行下去,這就是串行的概念。14.1.3串行,并發(fā)與并行同樣,可以用如下的代碼依次調(diào)用如下兩個(gè)函數(shù)。funcA();funcB()程序?qū)⑤敵觯篈BC123。從執(zhí)行結(jié)果中可以看出,函數(shù)funcB總是會(huì)在函數(shù)funcA執(zhí)行完后再執(zhí)行,也就是說,3個(gè)數(shù)字的輸出永遠(yuǎn)在3個(gè)字符的輸出之后。下面來考慮一種并發(fā)的機(jī)制,funcA和funcB作為兩個(gè)并列的函數(shù)會(huì)同時(shí)運(yùn)行。在這種情況下,數(shù)字和字符的輸出并沒有預(yù)定的先后關(guān)系:函數(shù)funcB可以在funcA之前運(yùn)行,產(chǎn)生"123ABC"的輸出;當(dāng)然,更多的時(shí)候是兩個(gè)函數(shù)交替運(yùn)行,而運(yùn)行結(jié)果交織在一起,"1A2B3C"、"12ABC3"、"1AB23C"等都是可能的輸出結(jié)果。14.1.3串行,并發(fā)與并行稍加觀察就可以發(fā)現(xiàn),雖然字母和數(shù)字之間有多種輸出的排列方式,但是函數(shù)funcA和函數(shù)funcB內(nèi)部的語句仍然是順序執(zhí)行的,即字母總是按字母表順序輸出的,數(shù)字總是按從小到大的順序輸出的。圖14-1形象地描述了并發(fā)和串行的概念及其關(guān)系。14.1.3串行,并發(fā)與并行在這個(gè)例子中,由于函數(shù)funcA和函數(shù)funcB需要將字符打印到同一個(gè)控制臺(tái)上,所以無法在同一時(shí)刻實(shí)現(xiàn)兩個(gè)函數(shù)真正地并行運(yùn)行。但是,如果假設(shè)這兩個(gè)函數(shù)之間不相互“影響”,那么在多核的處理器上,這兩個(gè)函數(shù)是可以并行執(zhí)行的,圖14-2可以幫助讀者理解并發(fā)和并行這個(gè)兩個(gè)概念的異同。在本章中,將從線程和進(jìn)程兩個(gè)級(jí)別上介紹Python中這種多任務(wù)并發(fā)和并行的編程實(shí)現(xiàn)。14.2Python中多線程編程簡介在本節(jié)中,將首先介紹如何創(chuàng)建和管理線程,然后介紹多任務(wù)編程中最重要的一個(gè)問題—“同步”的兩種解決方案,即鎖機(jī)制和使用Queue模塊構(gòu)造線程隊(duì)列。14.2.1線程的創(chuàng)建和管理Python中提供了兩個(gè)多線程模塊:_thread模塊和threading模塊。其中,前者是一個(gè)較為簡單且低層的多線程實(shí)現(xiàn),僅提供原始的線程及互斥鎖創(chuàng)建方法;而后者是對(duì)thread模塊的擴(kuò)展,使用threading模塊可以進(jìn)行更全面的線程管理。本節(jié)將首先簡單地介紹_thread模塊中的多線程創(chuàng)建方式,之后將重點(diǎn)幫助讀者了解threading模塊的使用。

使用_thread模塊創(chuàng)建線程_thread模塊提供了十分簡單的線程創(chuàng)建方法,只需要調(diào)用如下函數(shù)即可創(chuàng)建一個(gè)線程。_thread.start_new_thread(function,args[,kwargs])其中,第一個(gè)輸入?yún)?shù)function是一個(gè)函數(shù),該函數(shù)包含的是要在新創(chuàng)建的線程中執(zhí)行的代碼,該函數(shù)執(zhí)行完畢后,線程也將終止;args是一個(gè)元組,需要傳入函數(shù)的參數(shù),若沒有需要傳入的參數(shù),則需使用空元組;kwargs是一個(gè)可選的參數(shù)字典,以字典的方式指定傳入函數(shù)function的參數(shù)。有了這個(gè)函數(shù),編程者就可以實(shí)現(xiàn)函數(shù)funcA和funcB的并發(fā)執(zhí)行功能了,這里用向func傳入不同的參數(shù)的方法來分別實(shí)現(xiàn)funcA和funcB的功能importsys

import_thread

importtime

deffunc(output_list):

foriinoutput_list:

print(i,end='')

time.sleep(0.1)#為了使交錯(cuò)執(zhí)行的效果更明顯

defrunFunc():

_thread.start_new_thread(func,(['A','B','C'],))#完成funcA的功能

_thread.start_new_thread(func,([1,2,3],))#完成funcB的功能

time.sleep(1)#等待兩個(gè)線程執(zhí)行完畢

sys.stdout.flush()#刷新標(biāo)準(zhǔn)輸出緩沖區(qū),以顯示完整運(yùn)行結(jié)果

foriinrange(5):#觀察5次運(yùn)行結(jié)果

print("#%d:"%i,end='')

runFunc()

print()

使用_thread模塊創(chuàng)建線程可能讀者會(huì)注意到,為了保證完整地顯示輸出結(jié)果,在runFunc后有一行休眠語句,用于等待兩個(gè)線程執(zhí)行完畢后刷新緩沖區(qū)再返回主程序。這種實(shí)現(xiàn)方式是十分笨拙且低效的(通常會(huì)因?yàn)闊o法預(yù)估線程的執(zhí)行時(shí)間而設(shè)置一個(gè)盡可能大的等待時(shí)間),在下面對(duì)threading模塊的介紹中,可以通過更高層且更便捷的方式實(shí)現(xiàn)這種“等待線程終止”及其他相關(guān)的線程管理功能。

使用threading模塊創(chuàng)建和管理線程使用threading模塊創(chuàng)建一個(gè)線程比使用thread模塊略微復(fù)雜一些,需要進(jìn)行以下三步。(1)定義threading.Thread的一個(gè)子類。(2)重寫該子類的初始化函數(shù)__init__(self[,args]),指明在新線程執(zhí)行前需要完成的工作。(3)重寫該子類的run(self,[,args])函數(shù),實(shí)現(xiàn)希望該線程在開始執(zhí)行時(shí)要完成的功能。除了上述兩個(gè)需要自定義的函數(shù)之外,threading.Thread類還提供了以下函數(shù)用以管理創(chuàng)建的進(jìn)程。(1)start():調(diào)用該函數(shù)將開始一個(gè)線程的執(zhí)行。注意,一個(gè)線程只可以被執(zhí)行一次,第二次調(diào)用同一個(gè)線程的start()函數(shù)將得到異常。(2)join():該函數(shù)用來等待線程的終止。(3)is_alive():該函數(shù)用于測試線程是否還在執(zhí)行(指run函數(shù)從開始執(zhí)行后直到終止前的狀態(tài))。

使用threading模塊創(chuàng)建和管理線程這里使用join函數(shù)的調(diào)用替代了原來runFunc中通過睡眠(sleep)等待線程終止的方式。這樣做不需要提前預(yù)估線程的執(zhí)行時(shí)間,可使程序更加健壯而高效,且增加了代碼的可讀性。同時(shí),threading模塊還提供了下列靜態(tài)函數(shù)以方便管理全局所有的線程。(1)threading.active_count():該函數(shù)返回當(dāng)前正在執(zhí)行的線程數(shù)目。(2)threading.enumerate():該函數(shù)返回包含所有正在執(zhí)行線程的列表。(3)threading.current_thread():返回當(dāng)前的線程對(duì)象。importthreading

importtime

importsys

classmyThread(threading.Thread):

def__init__(self,output_list):

threading.Thread.__init__(self)

self.output_list=output_list#初始化輸出列表

defrun(self):

foriinself.output_list:

print(i,end='')

time.sleep(0.1)#使交錯(cuò)執(zhí)行的效果更佳明顯

defrunFunc():

#創(chuàng)建線程

thread1=myThread(['A','B','C'])

thread2=myThread([1,2,3])

#開始線程的執(zhí)行

thread1.start()

thread2.start()

#等待線程的終止

thread1.join()

thread2.join()

sys.stdout.flush()#刷新標(biāo)準(zhǔn)輸出緩沖區(qū)

臨界區(qū)和臨界資源如前面描述的,同一進(jìn)程內(nèi)的多個(gè)線程共享數(shù)據(jù),然而,這在方便了線程間通信的同時(shí)也帶來了一個(gè)并發(fā)訪問共享資源時(shí)的沖突問題—線程同步。線程同步中最主要的問題在于程序?qū)εR界資源與臨界區(qū)的合理協(xié)調(diào)管控,其中,臨界資源是指同一時(shí)間只允許一個(gè)線程訪問的資源,而臨界區(qū)是指同一時(shí)間只允許一個(gè)線程執(zhí)行的一部分代碼區(qū)域,通常這個(gè)區(qū)域內(nèi)會(huì)包含對(duì)臨界資源的共享使用。例如,前面例程中的控制臺(tái)就可以看作是一個(gè)臨界資源,同一時(shí)間內(nèi)不可以有兩個(gè)線程同時(shí)向控制臺(tái)緩沖區(qū)寫入內(nèi)容。但這一臨界資源是由系統(tǒng)處理的,我們?cè)诖a中還會(huì)遇到一些自定義的臨界資源,共享變量便是其中一種,我們需要在程序中謹(jǐn)慎小心地處理這些臨界資源,否則程序結(jié)果將與預(yù)期產(chǎn)生很大偏差。

臨界區(qū)和臨界資源觀察程序輸出結(jié)果,可以發(fā)現(xiàn)在500個(gè)線程執(zhí)行之后,全局變量count的值不是500而是481(這一數(shù)字可能會(huì)因每次運(yùn)行而不同)。這是因?yàn)閷?duì)count的增一操作實(shí)際上并不是由一條底層機(jī)器指令完成的(術(shù)語稱為“不是原子操作”),讀者可以理解為該條語句被分解為以下3條語句。tmp<-count #首先,將內(nèi)存中count的值取到一個(gè)加法寄存器tmp中tmp<-tmp+1 #將tmp增1count<-tmp #將增1后的tmp寫回至內(nèi)存中的count變量上當(dāng)有多個(gè)線程同時(shí)執(zhí)行count的增一操作時(shí),會(huì)出現(xiàn)兩個(gè)線程同時(shí)取出count的值到兩個(gè)不同的tmp寄存器中,然后分別增1后寫回的情況,這樣兩個(gè)線程的運(yùn)行只會(huì)造成count增加1而非2;這也不是唯一造成結(jié)果的原因,甚至?xí)l(fā)生以下情況:一個(gè)線程取到count中的值后因?yàn)榕抨?duì)沒有及時(shí)將增1后的tmp值寫回,直到若干個(gè)進(jìn)程從開始到執(zhí)行完畢后才將tmp值寫回,這樣導(dǎo)致這些進(jìn)程對(duì)count的增加都變成無效的操作。這里,全局共享的count變量就是臨界資源,而count的增1指令就是臨界區(qū)內(nèi)的語句。可見,合理的控制對(duì)共享臨界資源的訪問和臨界區(qū)代碼的執(zhí)行是非常重要的,下面將介紹如何用鎖機(jī)制來解決線程同步的問題。importthreading,sys,time

count=0

classmyThread(threading.Thread):

defrun(self):

globalcount

time.sleep(0.1)

threadLock.acquire()#獲得鎖

count+=1#臨界區(qū)代碼

threadLock.release()#釋放鎖

threadLock=threading.Lock()#建立鎖對(duì)象

foriinrange(500):

thread=myThread()

thread.start()

foriinrange(500):

thread.join()

print(count)

互斥鎖鎖機(jī)制是處理同步問題的常用方法,其思想是保證臨界區(qū)只有一個(gè)線程可以進(jìn)入,一旦進(jìn)入則該臨界區(qū)域(即開始執(zhí)行臨界區(qū)域內(nèi)的代碼)就被“鎖上”,直到該線程釋放這個(gè)鎖,其他線程才可進(jìn)入此臨界區(qū)域。在Python中,threading模塊提供了Lock互斥鎖類,該對(duì)象的acquire()方法可以實(shí)現(xiàn)對(duì)臨界區(qū)域的“鎖定”,release()方法可以實(shí)現(xiàn)對(duì)臨界區(qū)域的“解鎖”。下面將鎖機(jī)制加入到前面的計(jì)數(shù)器例子中,以實(shí)現(xiàn)計(jì)數(shù)器對(duì)進(jìn)程數(shù)的準(zhǔn)確統(tǒng)計(jì),如代碼清單14-4所示。

互斥鎖可以看到,此時(shí)計(jì)數(shù)器工作正常,輸出結(jié)果和預(yù)期一致。當(dāng)一個(gè)線程在執(zhí)行count增1操作前會(huì)先調(diào)用acquire方法請(qǐng)求獲得鎖將臨界區(qū)“鎖上”,而如果之前有線程已經(jīng)獲得了鎖,即已經(jīng)將臨界區(qū)“鎖定”,那么這個(gè)請(qǐng)求將被放入一個(gè)等待隊(duì)列,直到獲得了鎖的線程執(zhí)行完畢臨界區(qū)中的代碼,調(diào)用release方法將鎖釋放,才可以批準(zhǔn)等待隊(duì)列中的一個(gè)線程的鎖請(qǐng)求。這樣,可以保證一次只有一個(gè)線程對(duì)共享變量count進(jìn)行訪問,完成對(duì)其的讀寫。importthreading,sys,time

count=0

classmyThread(threading.Thread):

defrun(self):

globalcount

time.sleep(0.1)

threadLock.acquire()#獲得鎖

count+=1#臨界區(qū)代碼

threadLock.release()#釋放鎖

threadLock=threading.Lock()#建立鎖對(duì)象

foriinrange(500):

thread=myThread()

thread.start()

foriinrange(500):

thread.join()

print(count)

互斥鎖鎖機(jī)制可能會(huì)帶來一些新的問題。其中最常見的問題就是死鎖,即兩個(gè)或兩個(gè)以上的線程在執(zhí)行過程中因爭奪資源而導(dǎo)致的互相等待的現(xiàn)象。importthreading,sys,time

count=0

classmyThread(threading.Thread):

defrun(self):

globalcount

time.sleep(0.1)

threadLock.acquire()#獲得鎖

count+=1#臨界區(qū)代碼

threadLock.release()#釋放鎖

threadLock=threading.Lock()#建立鎖對(duì)象

foriinrange(500):

thread=myThread()

thread.start()

foriinrange(500):

thread.join()

print(count)14.2.3queue模塊:隊(duì)列同步雖然使用threading模塊中提供的功能可以完成大多數(shù)線程同步的需求,然而依靠互斥鎖等機(jī)制實(shí)現(xiàn)線程同步是十分復(fù)雜的,且對(duì)于初學(xué)者來說稍不注意就會(huì)導(dǎo)致死鎖等問題的產(chǎn)生。但幸運(yùn)的是,Python中實(shí)現(xiàn)了可以支持多線程共享的隊(duì)列模塊—queue,用戶可以簡單地使用其提供的數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)線程同步。下面先簡單地介紹一下該模塊。queue模塊中提供了3種數(shù)據(jù)結(jié)構(gòu):先入先出隊(duì)列Queue類,后入先出“隊(duì)列”(其實(shí)它是棧數(shù)據(jù)結(jié)構(gòu),而非隊(duì)列)LifoQueue類和按優(yōu)先級(jí)高低決定出隊(duì)順序的優(yōu)先級(jí)隊(duì)列PriorityQueue類。這3種數(shù)據(jù)結(jié)構(gòu)都實(shí)現(xiàn)了鎖原語,能夠在多線程中直接使用,即當(dāng)多個(gè)線程同時(shí)執(zhí)行這些數(shù)據(jù)結(jié)構(gòu)的入隊(duì)、出隊(duì)等操作時(shí),它們都會(huì)自動(dòng)保證多個(gè)線程不發(fā)生沖突。queue模塊的Queue類有以下幾個(gè)常用方法,其他兩個(gè)類與此類似。14.2.3queue模塊:隊(duì)列同步(1)Queue.get():從隊(duì)列中獲取一個(gè)元素,并將其從隊(duì)列中刪除(出隊(duì))。(2)Queue.put(item):將item添加到隊(duì)列中(入隊(duì))。(3)Queue.qsize():返回隊(duì)列的大小。(4)Queue.empty():判斷隊(duì)列是否為空,若空則返回True,反之返回False。(5)Queue.full():如果隊(duì)列已滿(即隊(duì)列大小等于由對(duì)象創(chuàng)建時(shí)指定的maxsize,若創(chuàng)建時(shí)沒有給出或小于等于0,則認(rèn)為是無限長的隊(duì)列),則返回True,反之返回False。(6)Queue.join():阻塞調(diào)用線程,直到隊(duì)列中的所有任務(wù)被處理掉。(7)Queue.task_done():在完成隊(duì)列中的某項(xiàng)工作之后,需用該函數(shù)向隊(duì)列發(fā)送一個(gè)信號(hào),以幫助因Queue.join()阻塞的線程判斷隊(duì)列中的任務(wù)已全部被完成,從而不必阻塞地繼續(xù)運(yùn)行下去。14.2.3queue模塊:隊(duì)列同步在上述代碼中,創(chuàng)建了3個(gè)爬蟲線程ThreadUrl并發(fā)完成隊(duì)列Queue中指定URL的網(wǎng)頁爬取任務(wù),并使用隊(duì)列Queue的join方法等待由其指定的URL爬取任務(wù)的完成。在每個(gè)線程中,首先使用get方法從任務(wù)隊(duì)列Queue中得到一個(gè)URL,隨后爬取該URL對(duì)應(yīng)的網(wǎng)頁內(nèi)容并輸出,最后使用task_done方法通知被join方法所阻塞的主線程,以判斷Queue隊(duì)列的任務(wù)是否已全部完成,也就是說,當(dāng)最后一個(gè)線程完成任務(wù)并調(diào)用task_done方法(也是第5次調(diào)用task_done)后,所有任務(wù)完成,主線程接觸阻塞,繼續(xù)執(zhí)行下面的代碼,輸出“Done!”。注意,在進(jìn)程運(yùn)行前已經(jīng)使用setDaemon方法將進(jìn)程設(shè)置為守護(hù)線程,使得主線程在結(jié)束時(shí)會(huì)終結(jié)所有守護(hù)線程。(Python中主線程會(huì)等待所有非守護(hù)線程終止后再終止。)這種方式創(chuàng)建了一種簡單的方式以控制程序流程,因?yàn)橹恍鑼?duì)隊(duì)列執(zhí)行join操作后即可退出主線程,而不用手動(dòng)終結(jié)所有線程。在類似的程序中,還可以根據(jù)不同任務(wù)的重要程度,使用優(yōu)先隊(duì)列PriorityQueue實(shí)現(xiàn)重要任務(wù)先分發(fā)給線程,而不太重要的任務(wù)后分發(fā)給線程的功能。為了指定優(yōu)先級(jí),可以使用PriorityQueue.put((priority,item))的方式插入元素item并給予其優(yōu)先級(jí)priority(值越小,表明優(yōu)先級(jí)越高)。importqueue,\

threading,urllib.request

hosts=["","",

"",""]#待爬取的URL列表

Queue=queue.Queue()

classThreadUrl(threading.Thread):

def__init__(self,queue):

threading.Thread.__init__(self)

self.queue=queue

defrun(self):

whileTrue:

#從任務(wù)隊(duì)列中取出一個(gè)URL

host=self.queue.get()

#爬取頁面內(nèi)容

url=urllib.request.urlopen(host)

print(url.read(1024).decode("utf-8"))

#發(fā)出有一項(xiàng)任務(wù)已完成的信號(hào)

self.queue.task_done()

#建立爬蟲線程

foriinrange(3):

t=ThreadUrl(Queue)

t.setDaemon(True)#將進(jìn)程設(shè)置為守護(hù)進(jìn)程

t.start()

#將需要爬取的URL加入隊(duì)列

forhostinhosts:

Queue.put(host)

#等待所有線程任務(wù)完成

Queue.join()

print("Done!")14.3Python中的進(jìn)程編程14.3.1進(jìn)程的創(chuàng)建和終止Python在os模塊中提供了兩種進(jìn)程創(chuàng)建方式:system函數(shù)和exec家族函數(shù)。它們各有異同,分別適用于不同的進(jìn)程創(chuàng)建需求。在介紹完線程創(chuàng)建后,接下來介紹Python中終止進(jìn)程的方法。當(dāng)掌握進(jìn)程的創(chuàng)建和終止后,將帶領(lǐng)讀者編寫一個(gè)簡易的控制臺(tái)(類似于UNIXShell或者Windows中的命令行)。

使用system函數(shù)創(chuàng)建進(jìn)程os模塊的system函數(shù)是創(chuàng)建新進(jìn)程最為簡單的方式,其語法如下。status=system(command)其中,command是新創(chuàng)建的進(jìn)程將要執(zhí)行的字符串命令,status是表示新進(jìn)程是否正確執(zhí)行的返回值,若返回值status為0,則通常表示進(jìn)程創(chuàng)建運(yùn)行成功。利用該函數(shù)可以幫助用戶執(zhí)行系統(tǒng)命令,例如,下面的代碼可以創(chuàng)建一個(gè)新進(jìn)程執(zhí)行l(wèi)s指令(Linux命令),輸出當(dāng)前文件夾下的文件列表,這和在命令行中輸入ls是同樣的效果。ifos.system("ls")==0:print("以上是當(dāng)前文件夾下的文件列表.")

使用exec函數(shù)和fork函數(shù)創(chuàng)建子進(jìn)程exec家族包含8個(gè)類似的函數(shù),它們的參數(shù)輸入各有差別,但它們共同的特性是可以執(zhí)行新的程序替代原來的Python進(jìn)程,也就是說,這個(gè)函數(shù)執(zhí)行后,原來的Python進(jìn)程將不再存在,所以這個(gè)函數(shù)永遠(yuǎn)不會(huì)再返回。下面列出了8個(gè)exec家族函數(shù)的原型。os.execl(path,arg0,arg1,)os.execle(path,arg0,arg1,,env)os.execlp(file,arg0,arg1,)os.execlpe(file,arg0,arg1,,env)os.execv(path,args)os.execve(path,args,env)os.execvp(file,args)os.execvpe(file,args,env)其中,path用于指定新執(zhí)行程序的路徑,file表示要執(zhí)行的程序(在函數(shù)名沒有p的函數(shù)中,會(huì)在系統(tǒng)環(huán)境變量PATH中定位file程序;否則,需使用path指明完整路徑),args表示程序的輸入?yún)?shù),env可用字典方式設(shè)置新進(jìn)程執(zhí)行時(shí)的環(huán)境變量。

使用exec函數(shù)和fork函數(shù)創(chuàng)建子進(jìn)程一個(gè)簡單的例子是,當(dāng)執(zhí)行完自己的程序后,需要轉(zhuǎn)入另外一個(gè)程序的執(zhí)行(可以是任意可執(zhí)行程序,不必是Python程序,假設(shè)為“a.out”,則調(diào)用參數(shù)為“-a”)。此時(shí),exec家族的函數(shù)即可幫助用戶完成這項(xiàng)任務(wù),即創(chuàng)建一個(gè)新進(jìn)程替代自己原來的Python程序。示例代碼如下,importos#完成一些任務(wù)(代碼略去)os.execl('./a.out','-a')

#執(zhí)行a.out,接替原Python進(jìn)程但是,像讀者看到的,exec家族函數(shù)只是起到了替代原有進(jìn)程的作用,通常在實(shí)際使用中,該函數(shù)是和os模塊的fork函數(shù)配合使用以達(dá)到新進(jìn)程創(chuàng)建功能的。

使用exec函數(shù)和fork函數(shù)創(chuàng)建子進(jìn)程fork函數(shù)的功能是創(chuàng)建一個(gè)新的子進(jìn)程,與通常的函數(shù)不同,fork函數(shù)的一次執(zhí)行會(huì)有兩次返回,一次是在主進(jìn)程中(返回子進(jìn)程號(hào)),一次是在子進(jìn)程中(返回0)。通常而言,一個(gè)子進(jìn)程創(chuàng)建框架如下:pid=os.fork()ifpid==0:

實(shí)現(xiàn)子進(jìn)程完成的功能,例如,用exec家族函數(shù)執(zhí)行新程序 execl('./a.out','-a)else:

執(zhí)行主進(jìn)程接下來的任務(wù)這種由fork函數(shù)和exec函數(shù)配合使用的進(jìn)程創(chuàng)建方式與system函數(shù)提供的進(jìn)程創(chuàng)建方式是不同的,前者創(chuàng)建的子進(jìn)程會(huì)與主進(jìn)程并發(fā)執(zhí)行,而后者只能等system函數(shù)創(chuàng)建的新進(jìn)程執(zhí)行完畢并返回后,主進(jìn)程才可繼續(xù)執(zhí)行。

使用sys.exit函數(shù)終止進(jìn)程exit函數(shù)是常規(guī)的進(jìn)程終止方式,在進(jìn)程終止前,會(huì)執(zhí)行一些清理工作,同時(shí)將返回值返回給調(diào)用進(jìn)程(如os.system的返回值)。使用該返回值可以判斷程序是正確退出還是因異常而終止的。其函數(shù)調(diào)用語法如下:sys.exit(exit_code)其中,由exit_code指定返回給調(diào)用進(jìn)程的返回值。同時(shí),該函數(shù)的調(diào)用意味著自身進(jìn)程的終結(jié),所以和exec家族函數(shù)一樣,該函數(shù)調(diào)用也不會(huì)有返回值。14.3.2實(shí)例:編寫簡易的控制臺(tái)在cmd.py程序中,首先主進(jìn)程不斷讀取命令,若命令為空,則提示用戶輸入要執(zhí)行的命令;若命令為exit,則調(diào)用exit函數(shù)退出該程序。當(dāng)讀取到一條正常的指令時(shí),程序會(huì)調(diào)用system函數(shù)創(chuàng)建新進(jìn)程完成該指令,并利用其返回值判斷該指令執(zhí)行的成功與否。當(dāng)然,該程序的實(shí)現(xiàn)比較簡單,其實(shí)命令的解析和執(zhí)行工作仍是由system函數(shù)交給系統(tǒng)的控制臺(tái)程序執(zhí)行的。有興趣的讀者可以考慮嘗試使用fork函數(shù)和exec家族函數(shù)配合的方式實(shí)現(xiàn)一個(gè)自帶命令解析和執(zhí)行功能的控制臺(tái)程序。14.3.3使用subprocess進(jìn)行多進(jìn)程管理前面介紹的進(jìn)程創(chuàng)建和終止是較為底層的實(shí)現(xiàn)方式,使用它們進(jìn)行進(jìn)程的創(chuàng)建和管理將會(huì)十分復(fù)雜。Python在2.4版本之后引入了subprocess模塊,提供了較為高級(jí)的進(jìn)程管理功能。在subprocess模塊中,多進(jìn)程的管理功能主要源于Popen類的靈活使用,接下來將介紹這個(gè)類的使用方法??梢酝ㄟ^調(diào)用subprocess.Popen()函數(shù)來創(chuàng)建一個(gè)Popen類的對(duì)象,一個(gè)Popen對(duì)象對(duì)應(yīng)于一個(gè)新的子進(jìn)程,而用戶可通過對(duì)該對(duì)象的操作實(shí)現(xiàn)對(duì)進(jìn)程的管理。例如,可以用下面的代碼創(chuàng)建一個(gè)運(yùn)行ping命令的子進(jìn)程。14.3.3使用subprocess進(jìn)行多進(jìn)程管理importsubprocess#下面的代碼中,-c4表示發(fā)送四次ping報(bào)文(Windows中使用-n4)child=subprocess.Popen('ping–c4',shell=True)。Popen對(duì)象的常用屬性主要有以下幾個(gè)。(1)pid:子進(jìn)程的進(jìn)程號(hào)。(2)returncode:子進(jìn)程的返回值。如果進(jìn)程還沒有結(jié)束,則返回None。(3)stdin:子進(jìn)程的標(biāo)準(zhǔn)輸入流對(duì)象。(4)stdout:子進(jìn)程的標(biāo)準(zhǔn)輸出流對(duì)象。(5)stderr:子進(jìn)程的標(biāo)準(zhǔn)日志流對(duì)象。其中,后三個(gè)屬性可以在Popen的構(gòu)造函數(shù)中設(shè)置。Popen對(duì)象的常用成員函數(shù)主要有以下幾個(gè)。(1)wait():等待子進(jìn)程結(jié)束,設(shè)置并返回returncode屬性。例如,上面的例子調(diào)用child.wait()后將等待子進(jìn)程發(fā)送4次ping報(bào)文并完成結(jié)果統(tǒng)計(jì)。(2)poll():用于檢查子進(jìn)程是否已經(jīng)結(jié)束,設(shè)置并返回returncode屬性。(3)kll():殺死子進(jìn)程。(4)terminate():停止子進(jìn)程(與kill方法在實(shí)現(xiàn)上略有差別)。(5)send_signal(signal):向子進(jìn)程發(fā)送信號(hào)。(6)communicate(input=None):與子進(jìn)程進(jìn)行交互。14.3.4進(jìn)程間通信進(jìn)程之間的數(shù)據(jù)獨(dú)立性使得進(jìn)程通信成為一個(gè)重要問題,本小節(jié)將主要介紹兩種簡單的進(jìn)程通信方式:信號(hào)和管道

使用信號(hào)進(jìn)行進(jìn)程通信信號(hào)處理是進(jìn)程間通信的一種方式。信號(hào)是操作系統(tǒng)提供的一種軟件中斷,是一種異步的通信方式。例如,控制臺(tái)用戶按下中斷鍵(Ctrl+C),操作系統(tǒng)就會(huì)生成一個(gè)中斷信號(hào)(SIGINT)并發(fā)送給當(dāng)前運(yùn)行的程序;應(yīng)用程序會(huì)檢查是否有信號(hào)傳來,當(dāng)發(fā)現(xiàn)了中斷信號(hào)后會(huì)調(diào)用該信號(hào)對(duì)應(yīng)的信號(hào)處理程序終結(jié)該進(jìn)程,完成系統(tǒng)向該進(jìn)程的通信過程。當(dāng)然,信號(hào)并不只局限于中斷信號(hào),還包括很多,用戶甚至可以自己定義。更重要的是,針對(duì)每個(gè)信號(hào),不同的程序可以設(shè)置不同的自定義信號(hào)處理程序(除少數(shù)幾個(gè)系統(tǒng)不允許自定義處理的信號(hào)之外)。在Python中,可以使用signal模塊中的signal函數(shù)定義進(jìn)程針對(duì)不同信號(hào)自定義的處理程序。例如,可以重定義當(dāng)前進(jìn)程對(duì)中斷信號(hào)(SIGINT)的處理。sigint_handler函數(shù)(原型為sigint_handler(signum,frame))展示了信號(hào)處理程序的函數(shù)原型,其中,參數(shù)signum是信號(hào),而frame是進(jìn)程棧的狀況。需要說明的是,signal模塊中提供了系統(tǒng)支持的多種信號(hào)(如上面的signal.SIGINT),這些信號(hào)其實(shí)只是數(shù)值,但使用該方式可以提高代碼的跨平臺(tái)可移植性,也增加了代碼的可讀性。如前面介紹的,使用Popen對(duì)象的send_signal函數(shù)可以向子進(jìn)程發(fā)送信號(hào)。事實(shí)上,os模塊中也提供了kill(pid,signal)函數(shù),可以向任意進(jìn)程發(fā)送信號(hào),其中pid為接收信號(hào)方的進(jìn)程號(hào),signal為要發(fā)送的信號(hào)。代碼清單14-9將展示如何實(shí)現(xiàn)主進(jìn)程與計(jì)數(shù)子進(jìn)程間的通信。

使用信號(hào)進(jìn)行進(jìn)程通信在上面的代碼中,將用戶自定義信號(hào)SIGUSR1(其實(shí)也可以用一個(gè)沒有被其他信號(hào)占用的數(shù)值代替)定義為計(jì)數(shù)器的加1信號(hào),SIGUSR2定義為計(jì)數(shù)器報(bào)數(shù)信號(hào),并使用signal函數(shù)制定了相應(yīng)的處理函數(shù)。該計(jì)數(shù)器將一直在while死循環(huán)中等待信號(hào)的到來,直到收到SIGINT信號(hào)時(shí)終止。importsignal,sys

count=0

#SIGUSR1處理程序

defadd(signum,frame):

globalcount

count+=1

print("計(jì)數(shù)器加1.")

#SIGUSR2處理程序

defshow(signum,frame):

print("計(jì)數(shù)器當(dāng)前值為%d."%count)

#SIGINT處理程序

defsigint_handler(signum,frame):

print("謝謝使用!

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

評(píng)論

0/150

提交評(píng)論