Python學(xué)習(xí)之線程池與GIL全局鎖詳解
線程池
線程池的創(chuàng)建 - concurrent
concurrent
是 Python 的內(nèi)置包,使用它可以幫助我們完成創(chuàng)建線程池的任務(wù)。
方法名 | 介紹 | 示例 |
---|---|---|
futures.ThreadPoolExecutor | 創(chuàng)建線程池 | tpool=ThreadPoolExecutor(max_workers) |
通過(guò)調(diào)用 concurrent 包的 futures 模塊的 ThreadPoolExecutor 類,通過(guò)實(shí)例化 ThreadPoolExecutor 實(shí)現(xiàn)創(chuàng)建線程池的對(duì)象,它有一個(gè)參數(shù)來(lái)設(shè)置 線程池的數(shù)量。這和創(chuàng)建進(jìn)程池設(shè)置的數(shù)量是完全相同的。
線程池的常用方法
接下里看一下線程池對(duì)象中都有哪些常用的方法 :
函數(shù)名 | 介紹 | 用法 |
---|---|---|
submit | 往線程池中添加任務(wù) | submit(target, args) |
done | 確認(rèn)線程池中的某個(gè)線程是否完成了任務(wù) | done() |
rsult | 獲取當(dāng)前線程執(zhí)行任務(wù)的結(jié)果 | result() |
- submit 函數(shù):通過(guò) submit 函數(shù)將參數(shù)傳入;該函數(shù)傳入的參數(shù)也是傳入要執(zhí)行的函數(shù)與該函數(shù)的參數(shù),由于它的參數(shù)并不用需要通過(guò)賦值語(yǔ)句的形式傳入,只需要把相應(yīng)的值傳入就可以了(稍后會(huì)進(jìn)行一個(gè)練習(xí))。
- done 函數(shù):判斷當(dāng)前線程是否執(zhí)行完成;返回值是 bool 類型。
- result 函數(shù):返回當(dāng)前線程池中線程任務(wù)的執(zhí)行結(jié)果,通過(guò)這種方法就可以獲取線程池的返回值了。
線程池演示案例
1、定義一個(gè)函數(shù)實(shí)現(xiàn)循環(huán)的效果
2、定義一個(gè)線程池,設(shè)置線程的數(shù)量
# coding:utf-8 import time from concurrent.futures import ThreadPoolExecutor def work(i): print('第 {} 次循環(huán)'.format(i)) time.sleep(1) # 之所以每次都要使用 sleep 函數(shù),是因?yàn)楹瘮?shù)執(zhí)行太快;通過(guò) sleep 嘗試模擬一下長(zhǎng)時(shí)間的執(zhí)行一個(gè)任務(wù) if __name__ == '__main__': thread_poor = ThreadPoolExecutor(4) # 實(shí)例化一個(gè)線程池,設(shè)置線程數(shù)量為4 for i in range(20): thread_poor.submit(work, (i,)) # 利用 submit 函數(shù)將任務(wù)添加至 work 函數(shù)
運(yùn)行效果如下
從運(yùn)行結(jié)果來(lái)看,我們的線程任務(wù)每次執(zhí)行4個(gè)任務(wù),阻塞一秒后再執(zhí)行后續(xù)的四個(gè)線程的任務(wù),是沒(méi)有問(wèn)題的。
PS:需要注意的是,運(yùn)行結(jié)果有可能是出現(xiàn)將兩個(gè)或者多個(gè)任務(wù)的結(jié)果在同一行打印輸出,這是因?yàn)樵谕粫r(shí)間處理了多個(gè)線程的任務(wù),這也叫 "并發(fā)"。
線程鎖
前文的進(jìn)程池是與進(jìn)程鎖相對(duì)應(yīng)匹配的,同樣的線程池也有與之對(duì)應(yīng)的 線程鎖 。線程鎖的使用方法幾乎與進(jìn)程鎖是一樣的,只不過(guò)線程鎖對(duì)應(yīng)的是線程罷了。
1.實(shí)例化一個(gè)線程鎖
2、在 work 函數(shù)中調(diào)用線程鎖
3、并獲取 線程 的返回值(線程池也是可以獲取返回值的)
代碼示例如下:
# coding:utf-8 import os import time import threading from concurrent.futures import ThreadPoolExecutor lock = threading.Lock() # 全局定義一個(gè) Lock() 實(shí)例 def work(i): lock.acquire() # 區(qū)別于 進(jìn)程鎖 只需要在全局實(shí)例化一個(gè)即可,線程鎖需要在線程任務(wù)的函數(shù)中調(diào)用 線程鎖 才會(huì)生效 print('當(dāng)前是第 {} 次循環(huán)'.format(i)) time.sleep(1) # 之所以每次都要使用 sleep 函數(shù),是因?yàn)楹瘮?shù)執(zhí)行太快;通過(guò) sleep 嘗試模擬一下長(zhǎng)時(shí)間的執(zhí)行一個(gè)任務(wù) lock.release() return '第 {} 次循環(huán)的進(jìn)程id為:{}'.format(i, os.getpid()) # 線程也是基于進(jìn)程實(shí)現(xiàn)的 if __name__ == '__main__': thread_poor = ThreadPoolExecutor(4) # 實(shí)例化一個(gè)線程池,設(shè)置線程數(shù)量為4 result = [] for i in range(20): result_thread = thread_poor.submit(work, (i,)) # 利用 submit 函數(shù)將任務(wù)添加至 work 函數(shù); # 需要注意的是這里不像進(jìn)程池那樣使用賦值的形式傳入 work 函數(shù) result.append(result_thread) for res in result: print(res.result())
運(yùn)行結(jié)果如下:
從運(yùn)行結(jié)果可以看到,之前一同執(zhí)行的4個(gè)任務(wù)現(xiàn)在變成了一次只執(zhí)行一個(gè)任務(wù);每一個(gè)個(gè)線程都是在主進(jìn)程 93215下執(zhí)行的,說(shuō)明線程與進(jìn)程還是有所區(qū)別的,雖然我們有多個(gè)線程任務(wù)在執(zhí)行,但是依然是在主進(jìn)程下去完成的;同時(shí)我們還獲取到了 線程的返回值 第 {} 次循環(huán)的進(jìn)程id為:{}'.format(i, os.getpid() 。
以上就是線程池的使用和常用方法,我們會(huì)發(fā)現(xiàn)線程池的使用實(shí)際上要比進(jìn)程池的使用要容易一些。進(jìn)程池我們需要考慮 join 與 close 等一些問(wèn)題,但是線程池則不需要那么的嚴(yán)格,并且線程相對(duì)于進(jìn)程要更加的輕量,使用起來(lái)也更加的便捷。
利用線程池實(shí)現(xiàn)抽獎(jiǎng)小案例
案例代碼如下:
# coding:utf-8 import threading import random from concurrent.futures import ThreadPoolExecutor lock = threading.Lock() def luck_draw(arg): lock.acquire() # 從手機(jī)列表中隨機(jī)選出一個(gè)中獎(jiǎng)手機(jī),其他手機(jī)均未中獎(jiǎng) phone = random.choice(arg[0]) # 在從獎(jiǎng)池中隨機(jī)選取一個(gè)獎(jiǎng)品,視為該手機(jī)抽中的獎(jiǎng)品 price = random.choice(arg[1]) prices.remove(price) phones.remove(phone) lock.release() return '恭喜手機(jī)尾號(hào)為{}的用戶,抽到{}'.format(str(phone)[-5:-1], price) if __name__ == '__main__': t = ThreadPoolExecutor(3) # 通過(guò)創(chuàng)建三個(gè)線程從而實(shí)現(xiàn)每個(gè)線程完成一項(xiàng)抽獎(jiǎng)任務(wù) # 確定抽獎(jiǎng)人數(shù) phone_num = int(input('請(qǐng)輸入抽獎(jiǎng)的用戶人數(shù):')) # 模擬產(chǎn)生出相應(yīng)數(shù)量的手機(jī)號(hào) phones = random.sample(range(13300000000, 19999999999), phone_num) # 這里設(shè)置的隨機(jī)號(hào)碼僅做演示效果 prices = ['一等獎(jiǎng):iPhone12 ProMax', '二等獎(jiǎng):ipad2021pro', '三等獎(jiǎng):air wetter'] result = [] for i in range(3): # 三個(gè)任務(wù),每個(gè)線程分配一個(gè) t_result = t.submit(luck_draw, (phones, prices)) result.append(t_result) for res in result: print(res.result())
運(yùn)行效果如下:
GIL全局鎖
本章節(jié)的開頭我們就說(shuō)過(guò),該部分沒(méi)有代碼的相關(guān)練習(xí)。僅僅是對(duì) GIL全局鎖 做一個(gè)概念上的簡(jiǎn)單啟蒙。
其他語(yǔ)言的線程與Python線程的區(qū)別
多線程與多進(jìn)程的使用其實(shí)是比較復(fù)雜的,目前作為初學(xué)者來(lái)說(shuō)涉及的還比較淺。最近的幾個(gè)章節(jié)介紹了 進(jìn)程與線程在CPU的執(zhí)行方式,這里再進(jìn)行拓展一下。
下面我們看一張圖:
依然是一個(gè)CPU 與4個(gè)核心(可以認(rèn)為是4條跑道);
先看左邊的兩條跑道,是進(jìn)程1創(chuàng)建的3個(gè)線程。這三條線程有一個(gè)去了 1core 跑道,另外兩條則去了 2core 跑道。線程之間有選擇性的進(jìn)入了不同的跑道,當(dāng)然進(jìn)程1的主進(jìn)程或者說(shuō)是主線程可能會(huì)在 1core 跑道、也可能會(huì)在 2core 跑道,這是其他語(yǔ)言進(jìn)行多線程的樣子。
再來(lái)看看右邊,Python 創(chuàng)建的進(jìn)程2。當(dāng)進(jìn)程創(chuàng)建之后,包含主線程一共產(chǎn)生了3個(gè)線程,而這三個(gè)線程都跑到了 4core 跑道 上去。它不會(huì)像其他語(yǔ)言那樣去尋找不同的有空閑資源的跑道去執(zhí)行,而是僅僅在主進(jìn)程所處的跑道去執(zhí)行線程。造成 Python 中的多線程無(wú)法在多條跑道執(zhí)行任務(wù)的主要原因就是因?yàn)?GIL全局鎖 ,這個(gè) GIL 并不是 Python語(yǔ)法中添加上去的,而是 python解釋器 在執(zhí)行的時(shí)候自動(dòng)加了這把 "鎖" 。
GIL 的作用
因?yàn)?GIL 鎖 的關(guān)系,使得 Python 的多線程無(wú)法在多個(gè)CPU跑道上去執(zhí)行任務(wù),它只能在單一CPU上進(jìn)行工作。
這也限制了多線程的性能,畢竟 Python 的多線程只能在一條跑道上運(yùn)行。跑道滿了,運(yùn)行速度依然會(huì)慢。而在多個(gè)跑道上運(yùn)行的任務(wù)必然是要比單一跑道效率會(huì)高很多。
Python創(chuàng)始人 Guido 之所以保留 GIL 鎖,其實(shí)也是為了線程之間的安全。雖然這個(gè)話題一直都在爭(zhēng)論,不過(guò)我們也有辦法去掉這個(gè) GIL全局鎖。
默認(rèn)的解釋器是 Python 自帶的解釋器,這里我們可以選擇一個(gè)叫做 pypy 的解釋器。通過(guò)它來(lái)執(zhí)行 Python 腳本是不含有 GIL全局鎖 的,但并不太推薦這種做法。
另外一種解決方法就是使用 多進(jìn)程 + 多線程 的方式 來(lái)彌補(bǔ)這一短板上的問(wèn)題,通過(guò)多進(jìn)程在每個(gè) CPU 跑道上執(zhí)行任務(wù),并且每個(gè)進(jìn)程的跑道上再去執(zhí)行多個(gè)線程。 ,讓它們?cè)诟髯缘臅r(shí)間片上去運(yùn)行。這些用法會(huì)在后續(xù)的章節(jié)會(huì)介紹到,在此只需要了解即可。
以上就是Python學(xué)習(xí)之線程池與GIL全局鎖詳解的詳細(xì)內(nèi)容,更多關(guān)于Python線程池 GIL全局鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于PyQt5主窗口圖標(biāo)顯示問(wèn)題匯總
這篇文章主要介紹了關(guān)于PyQt5主窗口圖標(biāo)顯示問(wèn)題匯總,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03解決Pycharm在Debug的時(shí)候一直“Connected”沒(méi)有下一步動(dòng)作問(wèn)題
這篇文章主要介紹了解決Pycharm在Debug的時(shí)候一直“Connected”沒(méi)有下一步動(dòng)作問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08Python中應(yīng)用Winsorize縮尾處理的操作經(jīng)驗(yàn)
縮尾處理相當(dāng)于對(duì)數(shù)據(jù)進(jìn)行掐頭(尾)去尾,然后再按照一定的方法填補(bǔ)被掐掉的數(shù)據(jù),下面這篇文章主要給給大家介紹了關(guān)于Python中應(yīng)用Winsorize縮尾處理的相關(guān)資料,需要的朋友可以參考下2022-07-07Python中往列表中插入字典時(shí),數(shù)據(jù)重復(fù)問(wèn)題
這篇文章主要介紹了Python中往列表中插入字典時(shí),數(shù)據(jù)重復(fù)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-02-02python實(shí)現(xiàn)多個(gè)視頻文件合成畫中畫效果
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)多個(gè)視頻文件合成畫中畫效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08Python實(shí)現(xiàn)的樸素貝葉斯分類器示例
這篇文章主要介紹了Python實(shí)現(xiàn)的樸素貝葉斯分類器,結(jié)合具體實(shí)例形式分析了基于Python實(shí)現(xiàn)的樸素貝葉斯分類器相關(guān)定義與使用技巧,需要的朋友可以參考下2018-01-01網(wǎng)站滲透常用Python小腳本查詢同ip網(wǎng)站
這篇文章主要介紹了網(wǎng)站滲透常用Python小腳本查詢同ip網(wǎng)站,需要的朋友可以參考下2017-05-05python 創(chuàng)建一個(gè)空dataframe 然后添加行數(shù)據(jù)的實(shí)例
今天小編就為大家分享一篇python 創(chuàng)建一個(gè)空dataframe 然后添加行數(shù)據(jù)的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-06-06