摘要:進(jìn)程可創(chuàng)建多個(gè)線程來執(zhí)行同一程序的不同部分。就緒等待線程調(diào)度。運(yùn)行線程正常運(yùn)行阻塞暫停運(yùn)行,解除阻塞后進(jìn)入狀態(tài)重新等待調(diào)度。消亡線程方法執(zhí)行完畢返回或者異常終止。多線程多的情況下,依次執(zhí)行各線程的方法,前頭一個(gè)結(jié)束了才能執(zhí)行后面一個(gè)。
淺談Python多線程
作者簡介:
姓名:黃志成(小黃)線程 一.什么是線程?博客: 博客
操作系統(tǒng)原理相關(guān)的書,基本都會(huì)提到一句很經(jīng)典的話: "進(jìn)程是資源分配的最小單位,線程則是CPU調(diào)度的最小單位"。
線程是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。它被包含在進(jìn)程之中,是進(jìn)程中的實(shí)際運(yùn)作單位。一條線程指的是進(jìn)程中一個(gè)單一順序的控制流,一個(gè)進(jìn)程中可以并發(fā)多個(gè)線程,每條線程并行執(zhí)行不同的任務(wù)
好處 :
1.易于調(diào)度。 2.提高并發(fā)性。通過線程可方便有效地實(shí)現(xiàn)并發(fā)性。進(jìn)程可創(chuàng)建多個(gè)線程來執(zhí)行同一程序的不同部分。 3.開銷少。創(chuàng)建線程比創(chuàng)建進(jìn)程要快,所需開銷很少。 4.利于充分發(fā)揮多處理器的功能。通過創(chuàng)建多線程進(jìn)程,每個(gè)線程在一個(gè)處理器上運(yùn)行,從而實(shí)現(xiàn)應(yīng)用程序的并發(fā)性,使每個(gè)處理器都得到充分運(yùn)行。
在解釋python多線程的時(shí)候. 先和大家分享一下 python 的GIL 機(jī)制。
二.GIL(Global Interpreter Lock)全局解釋器鎖Python代碼的執(zhí)行由Python 虛擬機(jī)(也叫解釋器主循環(huán),CPython版本)來控制,Python 在設(shè)計(jì)之初就考慮到要在解釋器的主循環(huán)中,同時(shí)只有一個(gè)線程在執(zhí)行,即在任意時(shí)刻,只有一個(gè)線程在解釋器中運(yùn)行。對Python 虛擬機(jī)的訪問由全局解釋器鎖(GIL)來控制,正是這個(gè)鎖能保證同一時(shí)刻只有一個(gè)線程在運(yùn)行。
在多線程環(huán)境中,Python 虛擬機(jī)按以下方式執(zhí)行:
設(shè)置GIL
切換到一個(gè)線程去運(yùn)行
運(yùn)行:
a. 指定數(shù)量的字節(jié)碼指令,或者
b. 線程主動(dòng)讓出控制(可以調(diào)用time.sleep(0))
把線程設(shè)置為睡眠狀態(tài)
解鎖GIL
再次重復(fù)以上所有步驟
首先需要明確的一點(diǎn)是GIL并不是Python的特性,它是在實(shí)現(xiàn)Python解析器(CPython)時(shí)所引入的一個(gè)概念。Python同樣一段代碼可以通過CPython,PyPy,Psyco等不同的Python執(zhí)行環(huán)境來執(zhí)行。像其中的JPython就沒有GIL。然而因?yàn)镃Python是大部分環(huán)境下默認(rèn)的Python執(zhí)行環(huán)境。所以在很多人的概念里CPython就是Python,也就想當(dāng)然的把GIL歸結(jié)為Python語言的缺陷。所以這里要先明確一點(diǎn):GIL并不是Python的特性,Python完全可以不依賴于GIL
還有,就是在做I/O操作時(shí),GIL總是會(huì)被釋放。對所有面向I/O 的(會(huì)調(diào)用內(nèi)建的操作系統(tǒng)C 代碼的)程序來說,GIL 會(huì)在這個(gè)I/O 調(diào)用之前被釋放,以允許其它的線程在這個(gè)線程等待I/O 的時(shí)候運(yùn)行。如果是純計(jì)算的程序,沒有 I/O 操作,解釋器會(huì)每隔 100 次操作就釋放這把鎖,讓別的線程有機(jī)會(huì)執(zhí)行(這個(gè)次數(shù)可以通過 sys.setcheckinterval 來調(diào)整)如果某線程并未使用很多I/O 操作,它會(huì)在自己的時(shí)間片內(nèi)一直占用處理器(和GIL)。也就是說,I/O 密集型的Python 程序比計(jì)算密集型的程序更能充分利用多線程環(huán)境的好處。
三.線程的生命周期各個(gè)狀態(tài)說明:
New新建 :新創(chuàng)建的線程經(jīng)過初始化后,進(jìn)入Runnable狀態(tài)。
Runnable就緒:等待線程調(diào)度。調(diào)度后進(jìn)入運(yùn)行狀態(tài)。
Running運(yùn)行:線程正常運(yùn)行
Blocked阻塞:暫停運(yùn)行,解除阻塞后進(jìn)入Runnable狀態(tài)重新等待調(diào)度。
Dead消亡:線程方法執(zhí)行完畢返回或者異常終止。
可能有3種情況從Running進(jìn)入Blocked:
同步:線程中獲取同步鎖,但是資源已經(jīng)被其他線程鎖定時(shí),進(jìn)入Locked狀態(tài),直到該資源可獲?。ǐ@取的順序由Lock隊(duì)列控制)
睡眠:線程運(yùn)行sleep()或join()方法后,線程進(jìn)入Sleeping狀態(tài)。區(qū)別在于sleep等待固定的時(shí)間,而join是等待子線程執(zhí)行完。sleep()確保先運(yùn)行其他線程中的方法。當(dāng)然join也可以指定一個(gè)“超時(shí)時(shí)間”。從語義上來說,如果兩個(gè)線程a,b, 在a中調(diào)用b.join(),相當(dāng)于合并(join)成一個(gè)線程。將會(huì)使主調(diào)線程(即a)堵塞(暫停運(yùn)行, 不占用CPU資源), 直到被調(diào)用線程運(yùn)行結(jié)束或超時(shí), 參數(shù)timeout是一個(gè)數(shù)值類型,表示超時(shí)時(shí)間,如果未提供該參數(shù),那么主調(diào)線程將一直堵塞到被調(diào)線程結(jié)束。最常見的情況是在主線程中join所有的子線程。
等待:線程中執(zhí)行wait()方法后,線程進(jìn)入Waiting狀態(tài),等待其他線程的通知(notify)。wait方法釋放內(nèi)部所占用的瑣,同時(shí)線程被掛起,直至接收到通知被喚醒或超時(shí)(如果提供了timeout參數(shù)的話)。當(dāng)線程被喚醒并重新占有瑣的時(shí)候,程序才會(huì)繼續(xù)執(zhí)行下去。
threading.Lock()不允許同一線程多次acquire(), 而RLock允許, 即多次出現(xiàn)acquire和release
四.Python threading模塊上面介紹了這么多理論.下面我們用python提供的threading模塊來實(shí)現(xiàn)一個(gè)多線程的程序
threading 提供了兩種調(diào)用方式:
直接調(diào)用
import threading def func(n): # 定義每個(gè)線程要運(yùn)行的函數(shù) while n > 0: print("當(dāng)前線程數(shù):", threading.activeCount()) n -= 1 for x in range(5): t = threading.Thread(target=func, args=(2,)) # 生成一個(gè)線程實(shí)例,生成實(shí)例后 并不會(huì)啟動(dòng),需要使用start命令 t.start() #啟動(dòng)線程
繼承式調(diào)用
class MyThread(threading.Thread): # 繼承threading的Thread類 def __init__(self, num): threading.Thread.__init__(self) # 必須執(zhí)行父類的構(gòu)造方法 self.num = num # 傳入?yún)?shù) num def run(self): # 定義每個(gè)線程要運(yùn)行的函數(shù) while self.num > 0: print("當(dāng)前線程數(shù):", threading.activeCount()) self.num -= 1 for x in range(5): t = MyThread(2) # 生成實(shí)例,傳入?yún)?shù) t.start() #啟動(dòng)線程
兩種方式都可以調(diào)用我們的多線程方法。
五.子線程阻塞運(yùn)行下面的代碼,看看結(jié)果.
import threading def func(n): while n > 0: print("當(dāng)前線程數(shù):", threading.activeCount()) n -= 1 for x in range(5): t = threading.Thread(target=func, args=(2,)) t.start() print("主線程:", threading.current_thread().name)
運(yùn)行結(jié)果:
當(dāng)前線程數(shù): 2 當(dāng)前線程數(shù): 2 當(dāng)前線程數(shù): 2 當(dāng)前線程數(shù): 2 當(dāng)前線程數(shù): 2 當(dāng)前線程數(shù): 3 當(dāng)前線程數(shù): 3 當(dāng)前線程數(shù): 3 主線程: MainThread 當(dāng)前線程數(shù): 3 當(dāng)前線程數(shù): 3
那我們?nèi)绾巫枞泳€程讓他們運(yùn)行完,在繼續(xù)后面的操作呢.這個(gè)時(shí)候join()方法就派上用途了. 我們改寫代碼:
import threading def func(n): while n > 0: print("當(dāng)前線程數(shù):", threading.activeCount()) n -= 1 threads = [] #運(yùn)行的線程列表 for x in range(5): t = threading.Thread(target=func, args=(2,)) threads.append(t) # 將子線程追加到列表 t.start() for t in threads: t.join() print("主線程:", threading.current_thread().name)
join的原理就是依次檢驗(yàn)線程池中的線程是否結(jié)束,沒有結(jié)束就阻塞直到線程結(jié)束,如果結(jié)束則跳轉(zhuǎn)執(zhí)行下一個(gè)線程的join函數(shù)。
先看看這個(gè):
阻塞主進(jìn)程,專注于執(zhí)行多線程中的程序。
多線程多join的情況下,依次執(zhí)行各線程的join方法,前頭一個(gè)結(jié)束了才能執(zhí)行后面一個(gè)。
無參數(shù),則等待到該線程結(jié)束,才開始執(zhí)行下一個(gè)線程的join。
參數(shù)timeout為線程的阻塞時(shí)間,如 timeout=2 就是罩著這個(gè)線程2s 以后,就不管他了,繼續(xù)執(zhí)行下面的代碼。
六.線程鎖(互斥鎖)一個(gè)進(jìn)程可以開啟多個(gè)線程,那么多么多個(gè)進(jìn)程操作相同數(shù)據(jù),勢必會(huì)出現(xiàn)沖突.那如何避免這種問題呢?
import threading,time num = 10 #共享變量 def func(): global num lock.acquire() # 加鎖 num = num - 1 lock.release() # 解鎖 print(num) threads = [] lock = threading.Lock() #生成全局鎖 for x in range(10): t = threading.Thread(target=func) threads.append(t) t.start() for t in threads: t.join()
通過 threading.Lock() 我們可以申請一個(gè)鎖。然后 acquire 方法進(jìn)入臨界區(qū).操作完共享數(shù)據(jù) 使用 release 方法退出.
臨界區(qū)的概念: 百度百科
在這里補(bǔ)充一下:Python的Queue模塊是線程安全的.可以不對它加鎖操作.
聰明的同學(xué) 會(huì)發(fā)現(xiàn)一個(gè)問題? 咱們不是有 GIL 嗎 為什么還要加鎖?
這個(gè)問題問的好!我們下一節(jié),將對這個(gè)問題進(jìn)行探討.
七.LOCK 和 GILGIL的鎖是對于一個(gè)解釋器,只能有一個(gè)thread在執(zhí)行bytecode。所以每時(shí)每刻只有一條bytecode在被執(zhí)行一個(gè)thread。GIL保證了bytecode 這層面上是線程是安全的.
但是如果你有個(gè)操作一個(gè)共享 x += 1,這個(gè)操作需要多個(gè)bytecodes操作,在執(zhí)行這個(gè)操作的多條bytecodes期間的時(shí)候可能中途就換thread了,這樣就出現(xiàn)了線程不安全的情況了。
總結(jié):同一時(shí)刻CPU上只有單個(gè)執(zhí)行流不代表線程安全。
八.信號量互斥鎖 同時(shí)只允許一個(gè)線程更改數(shù)據(jù),而Semaphore是同時(shí)允許一定數(shù)量的線程更改數(shù)據(jù) ,比如廁所有3個(gè)坑,那最多只允許3個(gè)人上廁所,后面的人只能等里面有人出來了才能再進(jìn)去。
import threading,time num = 10 def func(): global num lock.acquire() time.sleep(2) num = num - 1 lock.release() print(num) threads = [] lock = threading.BoundedSemaphore(5) #最多允許5個(gè)線程同時(shí)運(yùn)行 for x in range(10): t = threading.Thread(target=func) threads.append(t) t.start() for t in threads: t.join() print("主線程:", threading.current_thread().name)
運(yùn)行一下上面的代碼.你會(huì)很明顯的發(fā)現(xiàn) 每次只執(zhí)行五個(gè)線程。
參考文獻(xiàn)淺談多進(jìn)程多線程的選擇: 文章鏈接python-多線程(原理篇): 文章鏈接
Python有GIL為什么還需要線程同步?: 文章鏈接
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/41500.html
摘要:在多線程的問題上面概念比較多,也需要慢慢理解,其實(shí)也在多線程的鎖的上面做了很多優(yōu)化,還有互斥同步和非互斥同步,還有很多概念,什么是自旋和自適應(yīng)自旋,鎖消除順便提一下,上面的字符串拼接的例子就是用到了這種優(yōu)化方式,鎖粗化,我們下次再繼續(xù)分享。 在我們平常的開發(fā)工作中,或多或少的都能接觸到多線程編程或者一些并發(fā)問題,隨著操作系統(tǒng)和系統(tǒng)硬件的升級,并發(fā)編程被越來越多的運(yùn)用到我們的開發(fā)中,我們...
摘要:哪吒社區(qū)技能樹打卡打卡貼函數(shù)式接口簡介領(lǐng)域優(yōu)質(zhì)創(chuàng)作者哪吒公眾號作者架構(gòu)師奮斗者掃描主頁左側(cè)二維碼,加入群聊,一起學(xué)習(xí)一起進(jìn)步歡迎點(diǎn)贊收藏留言前情提要無意間聽到領(lǐng)導(dǎo)們的談話,現(xiàn)在公司的現(xiàn)狀是碼農(nóng)太多,但能獨(dú)立帶隊(duì)的人太少,簡而言之,不缺干 ? 哪吒社區(qū)Java技能樹打卡?【打卡貼 day2...
摘要:并發(fā)設(shè)計(jì)的三大原則原子性原子性對共享變量的操作相對于其他線程是不可干擾的,即其他線程的執(zhí)行只能在該原子操作完成后或開始前執(zhí)行。發(fā)現(xiàn)兩個(gè)線程運(yùn)行結(jié)束后的值為。這就是在多線程情況下要求程序執(zhí)行的順序按照代碼的先后順序執(zhí)行的原因之一。 并發(fā)設(shè)計(jì)的三大原則 原子性 原子性:對共享變量的操作相對于其他線程是不可干擾的,即其他線程的執(zhí)行只能在該原子操作完成后或開始前執(zhí)行。 通過一個(gè)小例子理解 pu...
閱讀 3890·2021-11-25 09:43
閱讀 2215·2021-11-23 10:11
閱讀 1438·2021-09-29 09:35
閱讀 1377·2021-09-24 10:31
閱讀 2067·2019-08-30 15:48
閱讀 2400·2019-08-29 15:28
閱讀 459·2019-08-29 12:36
閱讀 3518·2019-08-28 18:12