作者簡(jiǎn)介:
姓名:黃志成(小黃)
博客: 博客
線程
一.什么是線程?
操作系統(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)行栖榨。對(duì)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ì)被釋放广料。對(duì)所有面向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ù),勢(shì)必會(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() 我們可以申請(qǐng)一個(gè)鎖呵晨。然后 acquire 方法進(jìn)入臨界區(qū).操作完共享數(shù)據(jù) 使用 release 方法退出.
臨界區(qū)的概念: 百度百科
在這里補(bǔ)充一下:Python的Queue模塊是線程安全的.可以不對(duì)它加鎖操作.
聰明的同學(xué) 會(huì)發(fā)現(xiàn)一個(gè)問題? 咱們不是有 GIL 嗎 為什么還要加鎖?
這個(gè)問題問的好熬尺!我們下一節(jié),將對(duì)這個(gè)問題進(jìn)行探討.
七.LOCK 和 GIL
GIL的鎖是對(duì)于一個(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í)行流不代表線程安全税手。
八.信號(hào)量
互斥鎖 同時(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為什么還需要線程同步?: 文章鏈接