Python多线程介绍及实例
1.進(jìn)程和線程的概念
1.1進(jìn)程
簡(jiǎn)單的說(shuō):進(jìn)程就是運(yùn)行著的程序。
我們寫的python程序(或者其他應(yīng)用程序比如畫筆、qq等),運(yùn)行起來(lái),就稱之為一個(gè)進(jìn)程在windows下面打開任務(wù)管理器,里面顯示了當(dāng)前系統(tǒng)上運(yùn)行著的進(jìn)程。
可以看到,我們系統(tǒng)中有很多的進(jìn)程運(yùn)行著,比如qq、搜狗輸入法等。
這些程序還沒(méi)有運(yùn)行的時(shí)候,它們的程序代碼文件存儲(chǔ)在磁盤中,就是那些擴(kuò)展名為 .exe 文件。
雙擊它們,這些 .exe 文件就被os加載到內(nèi)存中,運(yùn)行起來(lái),成為進(jìn)程
1.2.主線程概念
而系統(tǒng)中每個(gè)進(jìn)程里面至少包含一個(gè) 線程 。
線程是操作系統(tǒng)創(chuàng)建的,每個(gè)線程對(duì)應(yīng)一個(gè)代碼執(zhí)行的數(shù)據(jù)結(jié)構(gòu),保存了代碼執(zhí)行過(guò)程中的重要的狀態(tài)信息。
沒(méi)有線程,操作系統(tǒng)沒(méi)法管理和維護(hù) 代碼運(yùn)行的狀態(tài)信息。
所以沒(méi)有創(chuàng)建線程之前,操作系統(tǒng)是不會(huì)執(zhí)行我們的代碼的。
我們前面寫的Python程序,里面雖然沒(méi)有創(chuàng)建線程的代碼,但實(shí)際上,當(dāng)Python解釋器程序運(yùn)行起來(lái)(成為一個(gè)進(jìn)程),OS就自動(dòng)的創(chuàng)建一個(gè)線程,通常稱為主線程,在這個(gè)主線程里面執(zhí)行代碼指令。
當(dāng)解釋器執(zhí)行我們python程序代碼的時(shí)候。 我們的代碼就在這個(gè)主線程中解釋執(zhí)行。
比如:下面這個(gè)程序,運(yùn)行起來(lái)后,只有一個(gè)線程,就是主線程,在主線程里面,執(zhí)行代碼,順序下來(lái),一直執(zhí)行到結(jié)束, 主線程就退出了。 同時(shí)進(jìn)程也結(jié)束了。
fee = input('請(qǐng)輸入午餐費(fèi)用:') members = input('請(qǐng)輸入聚餐人姓名,以英文逗號(hào),分隔:')# 將人員放入一個(gè)列表 memberlist = members.split(',') # 得到人數(shù) headcount = len(memberlist) # 計(jì)算人均費(fèi)用 avgfee = int (fee) / headcount print(avgfee)1.2.3.多線程概念
現(xiàn)代計(jì)算機(jī)上面,CPU是多核的, 每個(gè)核都可以執(zhí)行代碼。
要運(yùn)行程序里面的代碼,操作系統(tǒng)就會(huì)分配一個(gè)CPU核心去執(zhí)行該代碼。
有的時(shí)候,我們希望,能夠讓更多的CPU核心同時(shí)執(zhí)行我們的程序里面的一些代碼。
假如,我們程序里面有個(gè)名為 compress 的函數(shù),執(zhí)行壓縮文件的任務(wù)。
現(xiàn)在有4個(gè)大文件,需要壓縮。
如果是一個(gè)CPU核心執(zhí)行這個(gè)函數(shù)(單線程的程序),壓縮一個(gè)文件要10秒鐘的話, 那么壓縮4個(gè)文件,就要40秒。
如果我們能夠讓 4個(gè)CPU核心 同時(shí) 執(zhí)行壓縮函數(shù), 理論上就只要 10秒。
有的時(shí)候, 我們有一批任務(wù)要執(zhí)行,而這些任務(wù)的執(zhí)行時(shí)間主要耗費(fèi)在 非CPU計(jì)算 上面。
比如,我們需要到 前程無(wú)憂 網(wǎng)站 抓取 python 開發(fā)相關(guān)的職位信息。
我們要抓取幾百個(gè)網(wǎng)頁(yè)的內(nèi)容, 執(zhí)行這些抓取信息的任務(wù)的代碼,時(shí)間主要耗費(fèi)在等待網(wǎng)站返回信息上面。 等待信息返回的時(shí)候CPU是空閑的。
如果我們像以前那樣 在一個(gè)線程里面,用一個(gè)循環(huán) 依次 獲取100個(gè)網(wǎng)頁(yè)的信息,如下
# 抓取 網(wǎng)頁(yè)的職位信息 def grabOnePage(url):print('代碼發(fā)起請(qǐng)求,抓取網(wǎng)頁(yè)信息,具體代碼省略')for pageIdx in range(1,101):url = f'https://search.51job.com/list/020000,000000,0000,00,9,99,python,2,{pageIdx}.html'grabOnePage(url)就會(huì)有很長(zhǎng)的時(shí)間耗費(fèi)在 等待服務(wù)器返回信息上面。
如果我們能用100個(gè)線程,同時(shí)運(yùn)行 獲取網(wǎng)頁(yè)信息的代碼, 理論上,可以100倍的減少執(zhí)行時(shí)間。
要讓多個(gè)CPU核心同時(shí)去執(zhí)行任務(wù),我們的程序必須 創(chuàng)建多個(gè)線程 ,讓 CPU 執(zhí)行 多個(gè)線程 對(duì)應(yīng)的代碼。
2.Python代碼中創(chuàng)建新線程
應(yīng)用程序必須 通過(guò)操作系統(tǒng)提供的 系統(tǒng)調(diào)用,請(qǐng)求操作系統(tǒng)分配一個(gè)新的線程。
python3 將 系統(tǒng)調(diào)用創(chuàng)建線程 的功能封裝在 標(biāo)準(zhǔn)庫(kù) threading 中。
大家來(lái)看下面的一段代碼
print('主線程執(zhí)行代碼') # 從 threading 庫(kù)中導(dǎo)入Thread類 from threading import Thread from time import sleep# 定義一個(gè)函數(shù),作為新線程執(zhí)行的入口函數(shù) def threadFunc(arg1,arg2):print('子線程 開始')print(f'線程函數(shù)參數(shù)是:{arg1}, {arg2}')sleep(5)print('子線程 結(jié)束')# 創(chuàng)建 Thread 類的實(shí)例對(duì)象, 并且指定新線程的入口函數(shù) thread = Thread(target=threadFunc,args=('參數(shù)1', '參數(shù)2'))# 執(zhí)行start 方法,就會(huì)創(chuàng)建新線程, # 并且新線程會(huì)去執(zhí)行入口函數(shù)里面的代碼。 # 這時(shí)候 這個(gè)進(jìn)程 有兩個(gè)線程了。 thread.start()# 主線程的代碼執(zhí)行 子線程對(duì)象的join方法, # 就會(huì)等待子線程結(jié)束,才繼續(xù)執(zhí)行下面的代碼 thread.join() print('主線程結(jié)束')運(yùn)行該程序,解釋器執(zhí)行到下面代碼時(shí)
thread = Thread(target=threadFunc,args=('參數(shù)1', '參數(shù)2'))創(chuàng)建了一個(gè)Thread實(shí)例對(duì)象,其中,Thread類的初始化參數(shù) 有兩個(gè)
target參數(shù) 是指定新線程的 入口函數(shù), 新線程創(chuàng)建后就會(huì) 執(zhí)行該入口函數(shù)里面的代碼,
args 指定了 傳給 入口函數(shù)threadFunc 的參數(shù)。 線程入口函數(shù) 參數(shù),必須放在一個(gè)元組里面,里面的元素依次作為入口函數(shù)的參數(shù)。
注意,上面的代碼只是創(chuàng)建了一個(gè)Thread實(shí)例對(duì)象, 但這時(shí),新的線程還沒(méi)有創(chuàng)建。
要?jiǎng)?chuàng)建線程,必須要調(diào)用 Thread 實(shí)例對(duì)象的 start方法 。也就是執(zhí)行完下面代碼的時(shí)候
thread.start()新的線程才創(chuàng)建成功,并開始執(zhí)行 入口函數(shù)threadFunc 里面的代碼。
有的時(shí)候, 一個(gè)線程需要等待其它的線程結(jié)束,比如需要根據(jù)其他線程運(yùn)行結(jié)束后的結(jié)果進(jìn)行處理。
這時(shí)可以使用 Thread對(duì)象的 join 方法
thread.join()如果一個(gè)線程A的代碼調(diào)用了 對(duì)應(yīng)線程B的Thread對(duì)象的 join 方法,線程A就會(huì)停止繼續(xù)執(zhí)行代碼,等待線程B結(jié)束。 線程B結(jié)束后,線程A才繼續(xù)執(zhí)行后續(xù)的代碼。
所以主線程在執(zhí)行上面的代碼時(shí),就暫停在此處, 一直要等到 新線程執(zhí)行完畢,退出后,才會(huì)繼續(xù)執(zhí)行后續(xù)的代碼。
join通常用于 主線程把任務(wù)分配給幾個(gè)子線程,等待子線程完成工作后,需要對(duì)他們?nèi)蝿?wù)處理結(jié)果進(jìn)行再處理。
就好像一個(gè)領(lǐng)導(dǎo)把任務(wù)分給幾個(gè)員工,等幾個(gè)員工完成工作后,他需要收集他們的提高報(bào)告,進(jìn)行后續(xù)處理。
這種情況,主線程必須子線程完成才能進(jìn)行后續(xù)操作,所以join就是 等待參數(shù)對(duì)應(yīng)的線程完成,才返回。
3.共享數(shù)據(jù)的訪問(wèn)控制
做多線程開發(fā),經(jīng)常遇到這樣的情況:多個(gè)線程里面的代碼 需要訪問(wèn) 同一個(gè) 公共的數(shù)據(jù)對(duì)象。
這個(gè)公共的數(shù)據(jù)對(duì)象可以是任何類型, 比如一個(gè) 列表、字典、或者自定義類的對(duì)象。
有的時(shí)候,程序 需要 防止線程的代碼 同時(shí)操作 公共數(shù)據(jù)對(duì)象。 否則,就有可能導(dǎo)致 數(shù)據(jù)的訪問(wèn)互相沖突影響。
請(qǐng)看一個(gè)例子。
我們用一個(gè)簡(jiǎn)單的程序模擬一個(gè)銀行系統(tǒng),用戶可以往自己的帳號(hào)上存錢。
對(duì)應(yīng)代碼如下:
from threading import Thread,Lock from time import sleepbank = {'byhy' : 0 }bankLock = Lock()# 定義一個(gè)函數(shù),作為新線程執(zhí)行的入口函數(shù) def deposit(theadidx,amount):# 操作共享數(shù)據(jù)前,申請(qǐng)獲取鎖bankLock.acquire()balance = bank['byhy']# 執(zhí)行一些任務(wù),耗費(fèi)了0.1秒sleep(0.1)bank['byhy'] = balance + amountprint(f'子線程 {theadidx} 結(jié)束')# 操作完共享數(shù)據(jù)后,申請(qǐng)釋放鎖bankLock.release()theadlist = [] for idx in range(10):thread = Thread(target = deposit,args = (idx,1))thread.start()# 把線程對(duì)象都存儲(chǔ)到 threadlist中theadlist.append(thread)for thread in theadlist:thread.join()print('主線程結(jié)束') print(f'最后我們的賬號(hào)余額為 {bank["byhy"]}')Lock 對(duì)象的acquire方法 是申請(qǐng)鎖。
每個(gè)線程在 操作共享數(shù)據(jù)對(duì)象之前,都應(yīng)該 申請(qǐng)獲取操作權(quán),也就是 調(diào)用該 共享數(shù)據(jù)對(duì)象對(duì)應(yīng)的鎖對(duì)象的acquire方法。
如果線程A 執(zhí)行如下代碼,調(diào)用acquire方法的時(shí)候,
bankLock.acquire()別的線程B 已經(jīng)申請(qǐng)到了這個(gè)鎖, 并且還沒(méi)有釋放,那么 線程A的代碼就在此處 等待 線程B 釋放鎖,不去執(zhí)行后面的代碼。
直到線程B 執(zhí)行了鎖的 release 方法釋放了這個(gè)鎖, 線程A 才可以獲取這個(gè)鎖,就可以執(zhí)行下面的代碼了。
如果這時(shí)線程B 又執(zhí)行 這個(gè)鎖的acquire方法, 就需要等待線程A 執(zhí)行該鎖對(duì)象的release方法釋放鎖, 否則也會(huì)等待,不去執(zhí)行后面的代碼。
4.daemon線程
大家執(zhí)行下面的代碼
from threading import Thread from time import sleepdef threadFunc():sleep(2)print('子線程 結(jié)束')thread = Thread(target=threadFunc) thread.start() print('主線程結(jié)束')可以發(fā)現(xiàn),主線程先結(jié)束,要過(guò)個(gè)2秒鐘,等子線程運(yùn)行完,整個(gè)程序才會(huì)結(jié)束退出。
因?yàn)?#xff1a;
Python程序中當(dāng)所有的 非daemon線程 結(jié)束了,整個(gè)程序才會(huì)結(jié)束
主線程是非daemon線程,啟動(dòng)的子線程缺省也是 非daemon線程 線程。
所以,要等到 主線程和子線程 都結(jié)束,程序才會(huì)結(jié)束。
我們可以在創(chuàng)建線程的時(shí)候,設(shè)置daemon參數(shù)值為True,如下
from threading import Thread from time import sleepdef threadFunc():sleep(2)print('子線程 結(jié)束')thread = Thread(target=threadFunc,daemon=True # 設(shè)置新線程為daemon線程) thread.start() print('主線程結(jié)束')再次運(yùn)行,可以發(fā)現(xiàn),只要主線程結(jié)束了,整個(gè)程序就結(jié)束了。因?yàn)橹挥兄骶€程是非daemon線程。
4.多進(jìn)程
Python 官方解釋器 的每個(gè)線程要獲得執(zhí)行權(quán)限,必須獲取一個(gè)叫 GIL (全局解釋器鎖) 的東西。
這就導(dǎo)致了 Python 的多個(gè)線程 其實(shí) 并不能同時(shí)使用 多個(gè)CPU核心。
所以如果是計(jì)算密集型的任務(wù),不能采用多線程的方式。
大家可以運(yùn)行一下如下代碼
from threading import Threaddef f():while True:b = 53*53if __name__ == '__main__':plist = []# 啟動(dòng)10個(gè)線程for i in range(10):p = Thread(target=f)p.start()plist.append(p)for p in plist:p.join()運(yùn)行后,打開任務(wù)管理器,可以發(fā)現(xiàn) 即使是啟動(dòng)了10個(gè)線程,依然只能占用一個(gè)CPU核心的運(yùn)算能力。
如下圖所示,我的電腦有4個(gè)核心,這個(gè)Python進(jìn)程占用了1個(gè)核心的運(yùn)行能力,所以下圖顯示25,表示 25% ,也就是 1/4的CPU占用率
如果需要利用電腦多個(gè)CPU核心的運(yùn)算能力,可以使用Python的多進(jìn)程庫(kù),如下
運(yùn)行后,打開任務(wù)管理器,可以發(fā)現(xiàn) 有3個(gè)Python進(jìn)程,其中主進(jìn)程CPU占用率為0,兩個(gè)子進(jìn)程CPU各占滿了一個(gè)核心的運(yùn)算能力。
如下圖所示
仔細(xì)看上面的代碼,可以發(fā)現(xiàn)和多線程的使用方式非常類似。
還有一個(gè)問(wèn)題,主進(jìn)程如何獲取 子進(jìn)程的 運(yùn)算結(jié)果呢?
可以使用多進(jìn)程庫(kù) 里面的 Manage 對(duì)象,如下
from multiprocessing import Process,Manager from time import sleepdef f(taskno,return_dict):sleep(2)# 存放計(jì)算結(jié)果到共享對(duì)象中return_dict[taskno] = tasknoif __name__ == '__main__':manager = Manager()# 創(chuàng)建 類似字典的 跨進(jìn)程 共享對(duì)象return_dict = manager.dict()plist = []for i in range(10):p = Process(target=f, args=(i,return_dict))p.start()plist.append(p)for p in plist:p.join()print('get result...')# 從共享對(duì)象中取出其他進(jìn)程的計(jì)算結(jié)果for k,v in return_dict.items():print (k,v)總結(jié)
以上是生活随笔為你收集整理的Python多线程介绍及实例的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Keras保存和载入训练好的模型和参数
- 下一篇: python:pandas之read_c