1. 線程簡介
- 線程(Thread)也叫輕量級進(jìn)程,是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位,它被包涵在進(jìn)程之中,是進(jìn)程中的實(shí)際運(yùn)作單位。
- 線程自己不擁有系統(tǒng)資源,只擁有一點(diǎn)兒在運(yùn)行中必不可少的資源,但它可與同屬一個(gè)進(jìn)程的其它線程共享進(jìn)程所擁有的全部資源。
- 一個(gè)線程可以創(chuàng)建和撤消另一個(gè)線程,同一進(jìn)程中的多個(gè)線程之間可以并發(fā)執(zhí)行。
2. 為什么要使用線程
- 同一進(jìn)程中的線程之間共享內(nèi)存:線程在程序中是獨(dú)立的、并發(fā)的執(zhí)行流。與分隔的進(jìn)程相比,進(jìn)程中線程之間的隔離程度要小,它們共享內(nèi)存、文件句柄和其他進(jìn)程應(yīng)有的狀態(tài)。
- 線程的并發(fā)性更高:因?yàn)榫€程的劃分尺度小于進(jìn)程,使得多線程程序的并發(fā)性高。進(jìn)程在執(zhí)行過程中擁有獨(dú)立的內(nèi)存單元,而多個(gè)線程共享內(nèi)存,從而極大地提高了程序的運(yùn)行效率。
- 線程比進(jìn)程的性能更好:這是由于同一個(gè)進(jìn)程中的線程共享同一個(gè)進(jìn)程的虛擬空間。線程共享的環(huán)境包括進(jìn)程代碼段、進(jìn)程的公有數(shù)據(jù)等,利用這些共享的數(shù)據(jù),線程之間很容易實(shí)現(xiàn)通信。
- 多線程資源消耗更?。翰僮飨到y(tǒng)在創(chuàng)建進(jìn)程時(shí),必須為該進(jìn)程分配獨(dú)立的內(nèi)存空間,并分配大量的相關(guān)資源,但創(chuàng)建線程則簡單得多。因此,使用多線程來實(shí)現(xiàn)并發(fā)比使用多進(jìn)程的性能要高得多。
3. 創(chuàng)建線程的方式
- thread模塊在python3中更名為_thread.而且將要廢棄。
- python3中通過threading模塊創(chuàng)建多線程,創(chuàng)建方法有兩種:
- 通過實(shí)例化threading.Thread對象來創(chuàng)建:threading.Thread(target=xxx, args=(xxx,xxx))
- 通過繼承threading.Thread類,并重寫run方法來創(chuàng)建:MyThread()
4. 守護(hù)線程
- 使用setDaemon(True)把所有的子線程都變成了主線程的守護(hù)線程,因此當(dāng)主進(jìn)程結(jié)束后,子線程也會(huì)隨之結(jié)束。所以當(dāng)主線程結(jié)束后,整個(gè)程序就退出了。
- 把子進(jìn)程設(shè)置為守護(hù)線程,必須在start()之前設(shè)置。
- 為了讓守護(hù)線程執(zhí)行結(jié)束之后,主線程再結(jié)束,我們可以使用join方法,讓主線程等待子線程執(zhí)行
- join()在start()之后。
5. 多線程共享全局變量
- 線程是進(jìn)程的執(zhí)行單元,進(jìn)程是系統(tǒng)分配資源的最小單位,所以在同一個(gè)進(jìn)程中的多線程是共享資源的。
6. 鎖
1. 互斥鎖
- 線程之間是進(jìn)行隨機(jī)調(diào)度,多個(gè)線程同時(shí)修改同一條數(shù)據(jù)時(shí)可能會(huì)出現(xiàn)臟數(shù)據(jù)。
- 因此出現(xiàn)了線程鎖,即同一時(shí)刻允許一個(gè)線程執(zhí)行操作。線程鎖用于鎖定資源。
- 由于線程之間是進(jìn)行隨機(jī)調(diào)度,如果有多個(gè)線程同時(shí)操作一個(gè)對象,如果沒有很好地保護(hù)該對象,會(huì)造成程序結(jié)果的不可預(yù)期,我們也稱此為“線程不安全”。
2. 遞歸鎖
- RLcok類的用法和Lock類一模一樣,但它支持嵌套,在多個(gè)鎖沒有釋放的時(shí)候一般會(huì)使用RLcok類。
7. 信號量(BoundedSemaphore類)
- 互斥鎖同時(shí)只允許一個(gè)線程更改數(shù)據(jù),而Semaphore是同時(shí)允許一定數(shù)量的線程更改數(shù)據(jù) ,
- 比如酒店有5個(gè)房間,那最多只允許5個(gè)人開房,后面的人只能等里面有人出來了才能再進(jìn)去。
8. 事件(Event類)
- python線程的事件用于主線程控制其他線程的執(zhí)行,事件是一個(gè)簡單的線程同步對象,其主要提供以下幾個(gè)方法:
- clear 將flag設(shè)置為“False”
- set 將flag設(shè)置為“True”
- is_set 判斷是否設(shè)置了flag
- wait 會(huì)一直監(jiān)聽flag,如果沒有檢測到flag就一直處于阻塞狀態(tài)
- 事件處理的機(jī)制:全局定義了一個(gè)“Flag”,當(dāng)flag值為“False”,那么event.wait()就會(huì)阻塞,當(dāng)flag值為“True”,那么event.wait()便不再阻塞。
9. GIL(Global Interpreter Lock)全局解釋器鎖
- GIL的全稱是Global Interpreter Lock(全局解釋器鎖).只是cpython解釋器導(dǎo)致與python語言無關(guān),用別的解釋器無此問題;
- 在python中,無論有多少核,同時(shí)只能執(zhí)行一個(gè)線程。究其原因,這就是由于GIL的存在導(dǎo)致的。
- cpython解釋器為了數(shù)據(jù)安全所做的決定。某個(gè)線程想要執(zhí)行,必須先拿到GIL,我們可以把GIL看作是“通行證”,并且在一個(gè)python進(jìn)程中,GIL只有一個(gè)。拿不到通行證的線程,就不允許進(jìn)入CPU執(zhí)行。
- Python多線程的工作過程:
- 拿到公共數(shù)據(jù)
- 申請GIL
- python解釋器調(diào)用os原生線程
- os操作cpu執(zhí)行運(yùn)算
- 當(dāng)該線程執(zhí)行時(shí)間到后,無論運(yùn)算是否已經(jīng)執(zhí)行完,gil都被要求釋放
- 進(jìn)而由其他進(jìn)程重復(fù)上面的過程
- 等其他進(jìn)程執(zhí)行完后,又會(huì)切換到之前的線程(從他記錄的上下文繼續(xù)執(zhí)行),整個(gè)過程是每個(gè)線程執(zhí)行自己的運(yùn)算,當(dāng)執(zhí)行時(shí)間到就進(jìn)行切換(context switch)。
- python針對不同類型的代碼執(zhí)行效率也是不同的:
- CPU密集型代碼(各種循環(huán)處理、計(jì)算等等),在這種情況下,由于計(jì)算工作多,ticks計(jì)數(shù)很快就會(huì)達(dá)到閾值,然后觸發(fā)GIL的釋放與再競爭(多個(gè)線程來回切換當(dāng)然是需要消耗資源的),所以python下的多線程對CPU密集型代碼并不友好??梢允褂枚噙M(jìn)程。
- IO密集型代碼(文件處理、網(wǎng)絡(luò)爬蟲等涉及文件讀寫的操作),多線程能夠有效提升效率(單線程下有IO操作會(huì)進(jìn)行IO等待,造成不必要的時(shí)間浪費(fèi),而開啟多線程能在線程A等待時(shí),自動(dòng)切換到線程B,可以不浪費(fèi)CPU的資源,從而能提升程序執(zhí)行效率)。所以python的多線程對IO密集型代碼比較友好。
- python下想要充分利用多核CPU,就用多進(jìn)程。因?yàn)槊總€(gè)進(jìn)程有各自獨(dú)立的GIL,互不干擾,這樣就可以真正意義上的并行執(zhí)行,在python中,多進(jìn)程的執(zhí)行效率優(yōu)于多線程(僅僅針對多核CPU而言)。
- GIL在python中的版本差異:
- 在python2.x里,GIL的釋放邏輯是當(dāng)前線程遇見IO操作或者ticks計(jì)數(shù)達(dá)到100時(shí)進(jìn)行釋放。(ticks可以看作是python自身的一個(gè)計(jì)數(shù)器,專門做用于GIL,每次釋放后歸零,這個(gè)計(jì)數(shù)可以通過sys.setcheckinterval 來調(diào)整)。而每次釋放GIL鎖,線程進(jìn)行鎖競爭、切換線程,會(huì)消耗資源。并且由于GIL鎖存在,python里一個(gè)進(jìn)程永遠(yuǎn)只能同時(shí)執(zhí)行一個(gè)線程(拿到GIL的線程才能執(zhí)行),這就是為什么在多核CPU上,python的多線程效率并不高。
- 在python3.x中,GIL不使用ticks計(jì)數(shù),改為使用計(jì)時(shí)器(執(zhí)行時(shí)間達(dá)到閾值后,當(dāng)前線程釋放GIL),這樣對CPU密集型程序更加友好,但依然沒有解決GIL導(dǎo)致的同一時(shí)間只能執(zhí)行一個(gè)線程的問題,所以效率依然不盡如人意。
10. 線程池
- 線程池在系統(tǒng)啟動(dòng)時(shí)即創(chuàng)建大量空閑的線程,程序只要將一個(gè)函數(shù)提交給線程池,線程池就會(huì)啟動(dòng)一個(gè)空閑的線程來執(zhí)行它。當(dāng)該函數(shù)執(zhí)行結(jié)束后,該線程并不會(huì)死亡,而是再次返回到線程池中變成空閑狀態(tài),等待執(zhí)行下一個(gè)函數(shù)。
- 此外,使用線程池可以有效地控制系統(tǒng)中并發(fā)線程的數(shù)量。當(dāng)系統(tǒng)中包含有大量的并發(fā)線程時(shí),會(huì)導(dǎo)致系統(tǒng)性能急劇下降,甚至導(dǎo)致 Python 解釋器崩潰,而線程池的最大線程數(shù)參數(shù)可以控制系統(tǒng)中并發(fā)線程的數(shù)量不超過此數(shù)。
- 官網(wǎng):https://docs.python.org/dev/library/concurrent.futures.html
- 從Python3.2開始,標(biāo)準(zhǔn)庫為我們提供了concurrent.futures模塊,它提供了ThreadPoolExecutor和ProcessPoolExecutor兩個(gè)類,實(shí)現(xiàn)了對threading和multiprocessing的進(jìn)一步抽象(這里主要關(guān)注線程池),不僅可以幫我們自動(dòng)調(diào)度線程,還可以做到:
- 主線程可以獲取某一個(gè)線程(或者任務(wù)的)的狀態(tài),以及返回值。
- 當(dāng)一個(gè)線程完成的時(shí)候,主線程能夠立即知道。
- 讓多線程和多進(jìn)程的編碼接口一致。
concurrent.futures模塊提供了高度封裝的異步調(diào)用接口
ThreadPoolExecutor:線程池,提供異步調(diào)用
屬性:max_workers, 線程池容量
1、submit(fn, *args, **kwargs)
異步提交任務(wù)
2、map(func, *iterables, timeout=None, chunksize=1)
取代for循環(huán)submit的操作
3、shutdown(wait=True)
相當(dāng)于進(jìn)程池的pool.close()+pool.join()操作
wait=True,等待池內(nèi)所有任務(wù)執(zhí)行完畢回收完資源后才繼續(xù)
wait=False,立即返回,并不會(huì)等待池內(nèi)的任務(wù)執(zhí)行完畢
但不管wait參數(shù)為何值,整個(gè)程序都會(huì)等到所有任務(wù)執(zhí)行完畢
submit和map必須在shutdown之前
4、result(timeout=None)
取得結(jié)果
5、add_done_callback(fn)
回調(diào)函數(shù)
6、done()
方法用于判定某個(gè)任務(wù)是否完成
7、cancel()
cancel方法用于取消某個(gè)任務(wù),該任務(wù)沒有放入線程池中才能取消成功
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。