引言&動機
考慮一下這個場景秀睛,我們有10000條數(shù)據(jù)需要處理噪舀,處理每條數(shù)據(jù)需要花費1秒,但讀取數(shù)據(jù)只需要0.1秒壕探,每條數(shù)據(jù)互不干擾冈钦。該如何執(zhí)行才能花費時間最短呢?
在多線程(MT)編程出現(xiàn)之前李请,電腦程序的運行由一個執(zhí)行序列組成瞧筛,執(zhí)行序列按順序在主機的中央處理器(CPU)中運行。無論是任務(wù)本身要求順序執(zhí)行還是整個程序是由多個子任務(wù)組成导盅,程序都是按這種方式執(zhí)行的较幌。即使子任務(wù)相互獨立,互相無關(guān)(即白翻,一個子任務(wù)的結(jié)果不影響其它子 任務(wù)的結(jié)果)時也是這樣乍炉。
對于上邊的問題,如果使用一個執(zhí)行序列來完成滤馍,我們大約需要花費 10000*0.1 + 10000 = 11000 秒岛琼。這個時間顯然是太長了。
那我們有沒有可能在執(zhí)行計算的同時取數(shù)據(jù)呢巢株?或者是同時處理幾條數(shù)據(jù)呢槐瑞?如果可以,這樣就能大幅提高任務(wù)的效率阁苞。這就是多線程編程的目的困檩。
對于本質(zhì)上就是異步的, 需要有多個并發(fā)事務(wù)那槽,各個事務(wù)的運行順序可以是不確定的悼沿,隨機的,不可預(yù)測的問題骚灸,多線程是最理想的解決方案糟趾。這樣的任務(wù)可以被分成多個執(zhí)行流,每個流都有一個要完成的目標,然后將得到的結(jié)果合并拉讯,得到最終的結(jié)果涤浇。
線程和進程
什么是進程
進程(有時被稱為重量級進程)是程序的一次 執(zhí)行鳖藕。每個進程都有自己的地址空間魔慷,內(nèi)存,數(shù)據(jù)棧以及其它記錄其運行軌跡的輔助數(shù)據(jù)著恩。操作系 統(tǒng)管理在其上運行的所有進程院尔,并為這些進程公平地分配時間。進程也可以通過 fork 和 spawn 操作 來完成其它的任務(wù)喉誊。不過各個進程有自己的內(nèi)存空間邀摆,數(shù)據(jù)棧等,所以只能使用進程間通訊(IPC)伍茄, 而不能直接共享信息栋盹。
什么是線程
線程(有時被稱為輕量級進程)跟進程有些相似,不同的是敷矫,所有的線程運行在同一個進程中例获, 共享相同的運行環(huán)境。它們可以想像成是在主進程或“主線程”中并行運行的“迷你進程”曹仗。
線程有開始榨汤,順序執(zhí)行和結(jié)束三部分。它有一個自己的指令指針怎茫,記錄自己運行到什么地方收壕。 線程的運行可能被搶占(中斷),或暫時的被掛起(也叫睡眠)轨蛤,讓其它的線程運行蜜宪,這叫做讓步。 一個進程中的各個線程之間共享同一片數(shù)據(jù)空間祥山,所以線程之間可以比進程之間更方便地共享數(shù)據(jù)以及相互通訊端壳。
當然,這樣的共享并不是完全沒有危險的枪蘑。如果多個線程共同訪問同一片數(shù)據(jù)损谦,則由于數(shù)據(jù)訪 問的順序不一樣,有可能導(dǎo)致數(shù)據(jù)結(jié)果的不一致的問題岳颇。這叫做競態(tài)條件(race condition)照捡。
線程一般都是并發(fā)執(zhí)行的,不過在單 CPU 的系統(tǒng)中话侧,真正的并發(fā)是不可能的栗精,每個線程會被安排成每次只運行一小會,然后就把 CPU 讓出來,讓其它的線程去運行悲立。由于有的函數(shù)會在完成之前阻塞住鹿寨,在沒有特別為多線程做修改的情 況下,這種“貪婪”的函數(shù)會讓 CPU 的時間分配有所傾斜薪夕。導(dǎo)致各個線程分配到的運行時間可能不 盡相同脚草,不盡公平。
Python原献、線程和全局解釋器鎖
全局解釋器鎖(GIL)
首先需要明確的一點是GIL并不是Python的特性馏慨,它是在實現(xiàn)Python解析器(CPython)時所引入的一個概念。就好比C++是一套語言(語法)標準姑隅,但是可以用不同的編譯器來編譯成可執(zhí)行代碼写隶。同樣一段代碼可以通過CPython,PyPy讲仰,Psyco等不同的Python執(zhí)行環(huán)境來執(zhí)行(其中的JPython就沒有GIL)慕趴。
那么CPython實現(xiàn)中的GIL又是什么呢?GIL全稱Global Interpreter Lock為了避免誤導(dǎo)鄙陡,我們還是來看一下官方給出的解釋:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
盡管Python完全支持多線程編程冕房, 但是解釋器的C語言實現(xiàn)部分在完全并行執(zhí)行時并不是線程安全的。 實際上柔吼,解釋器被一個全局解釋器鎖保護著毒费,它確保任何時候都只有一個Python線程執(zhí)行。
在多線程環(huán)境中愈魏,Python 虛擬機按以下方式執(zhí)行:
設(shè)置GIL
切換到一個線程去執(zhí)行
運行
指定數(shù)量的字節(jié)碼指令
線程主動讓出控制(可以調(diào)用time.sleep(0))
把線程設(shè)置完睡眠狀態(tài)
解鎖GIL
再次重復(fù)以上步驟
對所有面向 I/O 的(會調(diào)用內(nèi)建的操作系統(tǒng) C 代碼的)程序來說觅玻,GIL 會在這個 I/O 調(diào)用之 前被釋放,以允許其它的線程在這個線程等待 I/O 的時候運行培漏。如果某線程并未使用很多 I/O 操作溪厘, 它會在自己的時間片內(nèi)一直占用處理器(和 GIL)。也就是說牌柄,I/O 密集型的 Python 程序比計算密集 型的程序更能充分利用多線程環(huán)境的好處畸悬。
退出線程
當一個線程結(jié)束計算,它就退出了珊佣。線程可以調(diào)用 thread.exit()之類的退出函數(shù)蹋宦,也可以使用 Python 退出進程的標準方法,如 sys.exit()或拋出一個 SystemExit 異常等咒锻。不過冷冗,你不可以直接 “殺掉”(“kill”)一個線程。
在 Python 中使用線程
在 Win32 和 Linux, Solaris, MacOS, *BSD 等大多數(shù)類 Unix 系統(tǒng)上運行時惑艇,Python 支持多線程 編程蒿辙。Python 使用 POSIX 兼容的線程拇泛,即 pthreads。
默認情況下思灌,只要在解釋器中
>> import thread
如果沒有報錯俺叭,則說明線程可用。
Python 的 threading 模塊
Python 供了幾個用于多線程編程的模塊泰偿,包括 thread, threading 和 Queue 等熄守。thread 和 threading 模塊允許程序員創(chuàng)建和管理線程。thread 模塊 供了基本的線程和鎖的支持甜奄,而 threading 供了更高級別柠横,功能更強的線程管理的功能窃款。Queue 模塊允許用戶創(chuàng)建一個可以用于多個線程之間 共享數(shù)據(jù)的隊列數(shù)據(jù)結(jié)構(gòu)课兄。
核心 示:避免使用 thread 模塊
出于以下幾點考慮,我們不建議您使用 thread 模塊晨继。
更高級別的 threading 模塊更為先 進烟阐,對線程的支持更為完善,而且使用 thread 模塊里的屬性有可能會與 threading 出現(xiàn)沖突紊扬。其次蜒茄, 低級別的 thread 模塊的同步原語很少(實際上只有一個),而 threading 模塊則有很多餐屎。
對于你的進程什么時候應(yīng)該結(jié)束完全沒有控制檀葛,當主線程結(jié)束 時,所有的線程都會被強制結(jié)束掉腹缩,沒有警告也不會有正常的清除工作屿聋。我們之前說過,至少 threading 模塊能確保重要的子線程退出后進程才退出藏鹊。
thread 模塊
除了產(chǎn)生線程外润讥,thread 模塊也提供了基本的同步數(shù) 據(jù)結(jié)構(gòu)鎖對象(lock object,也叫原語鎖盘寡,簡單鎖楚殿,互斥鎖,互斥量竿痰,二值信號量)脆粥。
thread 模塊函數(shù)
start_new_thread(function, args, kwargs=None):產(chǎn)生一個新的線程,在新線程中用指定的參數(shù)和可選的 kwargs 來調(diào)用這個函數(shù)影涉。
allocate_lock():分配一個 LockType 類型的鎖對象
exit():讓線程退出
acquire(wait=None):嘗試獲取鎖對象
locked():如果獲取了鎖對象返回 True变隔,否則返回 False
release():釋放鎖
start_new_thread()要求一定要有前兩個參數(shù)。所以常潮,就算我們想要運行的函數(shù)不要參數(shù)弟胀,也要傳一個空的元組。
為什么要加上sleep(6)這一句呢? 因為,如果我們沒有讓主線程停下來孵户,那主線程就會運行下一條語句萧朝,顯示 “all done”,然后就關(guān)閉運行著 loop()和 loop1()的兩個線程夏哭,退出了检柬。
我們有沒有更好的辦法替換使用sleep() 這種不靠譜的同步方式呢?答案是使用鎖竖配,使用了鎖何址,我們就可以在兩個線程都退出之后馬上退出。
import thread
from time importsleep,time
loops=[4,2]
def loop(nloop,nsec,lock):
print('start loop %s at: %s'%(nloop,time()))
sleep(nsec)
print('loop %s done at: %s'%(nloop,time()))
# 每個線程都會被分配一個事先已經(jīng)獲得的鎖进胯,在 sleep()的時間到了之后就釋放 相應(yīng)的鎖以通知主線程用爪,這個線程已經(jīng)結(jié)束了。
lock.release()
def main():
print('starting at:',time())
locks=[]
nloops=range(len(loops))
foriinnloops:
# 調(diào)用 thread.allocate_lock()函數(shù)創(chuàng)建一個鎖的列表
lock=thread.allocate_lock()
# 分別調(diào)用各個鎖的 acquire()函數(shù)獲得, 獲得鎖表示“把鎖鎖上”
lock.acquire()
locks.append(lock)
foriinnloops:
# 創(chuàng)建線程胁镐,每個線程都用各自的循環(huán)號偎血,睡眠時間和鎖為參數(shù)去調(diào)用 loop()函數(shù)
thread.start_new_thread(loop,(i,loops[i],locks[i]))
foriinnloops:
# 在線程結(jié)束的時候,線程要自己去做解鎖操作
# 當前循環(huán)只是坐在那一直等(達到暫停主 線程的目的)盯漂,直到兩個鎖都被解鎖為止才繼續(xù)運行颇玷。
whilelocks[i].locked():pass
print('all DONE at:',time())
if__name__=='__main__':
main()
為什么我們不在創(chuàng)建鎖的循環(huán)里創(chuàng)建線程呢?有以下幾個原因:
我們想到實現(xiàn)線程的同步,所以要讓“所有的馬同時沖出柵欄”就缆。
獲取鎖要花一些時間帖渠,如果你的 線程退出得“太快”,可能會導(dǎo)致還沒有獲得鎖竭宰,線程就已經(jīng)結(jié)束了的情況空郊。
threading 模塊
threading 模塊不僅提供了 Thread 類,還 供了各 種非常好用的同步機制羞延。
下面是threading 模塊里所有的對象:
Thread: 表示一個線程的執(zhí)行的對象
Lock: 鎖原語對象(跟 thread 模塊里的鎖對象相同)
RLock: 可重入鎖對象渣淳。使單線程可以再次獲得已經(jīng)獲得了的鎖(遞歸鎖定)。
Condition: 條件變量對象能讓一個線程停下來伴箩,等待其它線程滿足了某個“條件”入愧。 如,狀態(tài)的改變或值的改變嗤谚。
Event: 通用的條件變量棺蛛。多個線程可以等待某個事件的發(fā)生,在事件發(fā)生后巩步, 所有的線程都會被激活旁赊。
Semaphore: 為等待鎖的線程 供一個類似“等候室”的結(jié)構(gòu)
BoundedSemaphore: 與 Semaphore 類似,只是它不允許超過初始值
Timer: 與 Thread 相似椅野,只是终畅,它要等待一段時間后才開始運行籍胯。
守護線程
另一個避免使用 thread 模塊的原因是,它不支持守護線程离福。當主線程退出時杖狼,所有的子線程不 論它們是否還在工作,都會被強行退出妖爷。有時蝶涩,我們并不期望這種行為,這時絮识,就引入了守護線程 的概念
threading 模塊支持守護線程绿聘,它們是這樣工作的:守護線程一般是一個等待客戶請求的服務(wù)器, 如果沒有客戶 出請求次舌,它就在那等著厢钧。如果你設(shè)定一個線程為守護線程幸乒,就表示你在說這個線程 是不重要的薛闪,在進程退出的時候肉盹,不用等待這個線程退出烹看。
如果你的主線程要退出的時候国拇,不用等待那些子線程完成,那就設(shè)定這些線程的 daemon 屬性惯殊。 即酱吝,在線程開始(調(diào)用 thread.start())之前,調(diào)用setDaemon()函數(shù)設(shè)定線程的 daemon 標志 (thread.setDaemon(True))就表示這個線程“不重要”
如果你想要等待子線程完成再退出土思,那就什么都不用做务热,或者顯式地調(diào)用 thread.setDaemon(False)以保證其 daemon 標志為 False。你可以調(diào)用thread.isDaemon()函數(shù)來判 斷其 daemon 標志的值己儒。新的子線程會繼承其父線程的 daemon 標志崎岂。整個 Python 會在所有的非守護 線程退出后才會結(jié)束,即進程中沒有非守護線程存在的時候才結(jié)束。
Thread 類
Thread類提供了以下方法:
run(): 用以表示線程活動的方法闪湾。
start():啟動線程活動冲甘。
join([time]): 等待至線程中止。這阻塞調(diào)用線程直至線程的join() 方法被調(diào)用中止-正常退出或者拋出未處理的異常-或者是可選的超時發(fā)生途样。
is_alive(): 返回線程是否活動的江醇。
name(): 設(shè)置/返回線程名。
daemon(): 返回/設(shè)置線程的 daemon 標志何暇,一定要在調(diào)用 start()函數(shù)前設(shè)置
用 Thread 類陶夜,你可以用多種方法來創(chuàng)建線程。我們在這里介紹三種比較相像的方法裆站。
創(chuàng)建一個Thread的實例条辟,傳給它一個函數(shù)
創(chuàng)建一個Thread的實例黔夭,傳給它一個可調(diào)用的類對象
從Thread派生出一個子類,創(chuàng)建一個這個子類的實例
下邊是三種不同方式的創(chuàng)建線程的示例:
import threading
from time importsleep,time
loops=[4,2]
def loop(nloop,nsec,lock):
print('start loop %s at: %s'%(nloop,time()))
sleep(nsec)
print('loop %s done at: %s'%(nloop,time()))
# 每個線程都會被分配一個事先已經(jīng)獲得的鎖羽嫡,在 sleep()的時間到了之后就釋放 相應(yīng)的鎖以通知主線程纠修,這個線程已經(jīng)結(jié)束了。
def main():
print('starting at:',time())
threads=[]
nloops=range(len(loops))
foriinnloops:
t=threading.Thread(target=loop,args=(i,loops[i]))
threads.append(t)
foriinnloops:
# start threads
threads[i].start()
foriinnloops:
# wait for all
# join()會等到線程結(jié)束厂僧,或者在給了 timeout 參數(shù)的時候扣草,等到超時為止。
# 使用 join()看上去 會比使用一個等待鎖釋放的無限循環(huán)清楚一些(這種鎖也被稱為"spinlock")
threads[i].join()# threads to finish
print('all DONE at:',time())
if__name__=='__main__':
main()
與傳一個函數(shù)很相似的另一個方法是在創(chuàng)建線程的時候颜屠,傳一個可調(diào)用的類的實例供線程啟動 的時候執(zhí)行——這是多線程編程的一個更為面向?qū)ο蟮姆椒ǔ矫睢O鄬τ谝粋€或幾個函數(shù)來說,由于類 對象里可以使用類的強大的功能甫窟,可以保存更多的信息密浑,這種方法更為靈活
from threading import Thread
from time importsleep,time
loops=[4,2]
classThreadFunc(object):
def __init__(self,func,args,name=""):
self.name=name
self.func=func
self.args=args
def __call__(self):
# 創(chuàng)建新線程的時候,Thread 對象會調(diào)用我們的 ThreadFunc 對象粗井,這時會用到一個特殊函數(shù) __call__()尔破。
self.func(*self.args)
def loop(nloop,nsec):
print('start loop %s at: %s'%(nloop,time()))
sleep(nsec)
print('loop %s done at: %s'%(nloop,time()))
def main():
print('starting at:',time())
threads=[]
nloops=range(len(loops))
foriinnloops:
t=Thread(target=ThreadFunc(loop,(i,loops[i]),loop.__name__))
threads.append(t)
foriinnloops:
# start threads
threads[i].start()
foriinnloops:
# wait for all
# join()會等到線程結(jié)束,或者在給了 timeout 參數(shù)的時候浇衬,等到超時為止懒构。
# 使用 join()看上去 會比使用一個等待鎖釋放的無限循環(huán)清楚一些(這種鎖也被稱為"spinlock")
threads[i].join()# threads to finish
print('all DONE at:',time())
if__name__=='__main__':
main()
最后一個例子介紹如何子類化 Thread 類,這與上一個例子中的創(chuàng)建一個可調(diào)用的類非常像耘擂。使 用子類化創(chuàng)建線程(第 29-30 行)使代碼看上去更清晰明了胆剧。
from threading import Thread
from time importsleep,time
loops=[4,2]
classMyThread(Thread):
def __init__(self,func,args,name=""):
super(MyThread,self).__init__()
self.name=name
self.func=func
self.args=args
def getResult(self):
returnself.res
def run(self):
# 創(chuàng)建新線程的時候,Thread 對象會調(diào)用我們的 ThreadFunc 對象醉冤,這時會用到一個特殊函數(shù) __call__()秩霍。
print'starting',self.name,'at:',time()
self.res=self.func(*self.args)
printself.name,'finished at:',time()
def loop(nloop,nsec):
print('start loop %s at: %s'%(nloop,time()))
sleep(nsec)
print('loop %s done at: %s'%(nloop,time()))
def main():
print('starting at:',time())
threads=[]
nloops=range(len(loops))
foriinnloops:
t=MyThread(loop,(i,loops[i]),loop.__name__)
threads.append(t)
foriinnloops:
# start threads
threads[i].start()
foriinnloops:
# wait for all
# join()會等到線程結(jié)束,或者在給了 timeout 參數(shù)的時候蚁阳,等到超時為止铃绒。
# 使用 join()看上去 會比使用一個等待鎖釋放的無限循環(huán)清楚一些(這種鎖也被稱為"spinlock")
threads[i].join()# threads to finish
print('all DONE at:',time())
if__name__=='__main__':
main()
除了各種同步對象和線程對象外,threading 模塊還 供了一些函數(shù)螺捐。
active_count(): 當前活動的線程對象的數(shù)量
current_thread(): 返回當前線程對象
enumerate(): 返回當前活動線程的列表
settrace(func): 為所有線程設(shè)置一個跟蹤函數(shù)
setprofile(func): 為所有線程設(shè)置一個 profile 函數(shù)
Lock & RLock
原語鎖定是一個同步原語颠悬,狀態(tài)是鎖定或未鎖定。兩個方法acquire()和release() 用于加鎖和釋放鎖归粉。
RLock 可重入鎖是一個類似于Lock對象的同步原語椿疗,但同一個線程可以多次調(diào)用。
Lock 不支持遞歸加鎖糠悼,也就是說即便在同 線程中届榄,也必須等待鎖釋放。通常建議改 RLock倔喂, 它會處理 “owning thread” 和 “recursion level” 狀態(tài)铝条,對于同 線程的多次請求鎖 為靖苇,只累加計數(shù)器。每次調(diào) release() 將遞減該計數(shù)器班缰,直到 0 時釋放鎖贤壁,因此 acquire() 和 release() 必須 要成對出現(xiàn)。
生產(chǎn)者-消費者問題和 Queue 模塊
現(xiàn)在我們用一個經(jīng)典的(生產(chǎn)者消費者)例子來介紹一下 Queue模塊埠忘。
生產(chǎn)者消費者的場景是: 生產(chǎn)者生產(chǎn)貨物脾拆,然后把貨物放到一個隊列之類的數(shù)據(jù)結(jié)構(gòu)中,生產(chǎn)貨物所要花費的時間無法預(yù)先確定莹妒。消費者消耗生產(chǎn)者生產(chǎn)的貨物的時間也是不確定的名船。
常用的 Queue 模塊的屬性:
queue(size): 創(chuàng)建一個大小為size的Queue對象。
qsize(): 返回隊列的大小(由于在返回的時候旨怠,隊列可能會被其它線程修改渠驼,所以這個值是近似值)
empty(): 如果隊列為空返回 True,否則返回 False
full(): 如果隊列已滿返回 True鉴腻,否則返回 False
put(item,block=0): 把item放到隊列中迷扇,如果給了block(不為0),函數(shù)會一直阻塞到隊列中有空間為止
get(block=0): 從隊列中取一個對象爽哎,如果給了 block(不為 0)蜓席,函數(shù)會一直阻塞到隊列中有對象為止
Queue 模塊可以用來進行線程間通訊,讓各個線程之間共享數(shù)據(jù)倦青。
現(xiàn)在瓮床,我們創(chuàng)建一個隊列,讓 生產(chǎn)者(線程)把新生產(chǎn)的貨物放進去供消費者(線程)使用产镐。
from Queue import Queue
from random import randint
from time importsleep,time
from threading import Thread
classMyThread(Thread):
def __init__(self,func,args,name=""):
super(MyThread,self).__init__()
self.name=name
self.func=func
self.args=args
def getResult(self):
returnself.res
def run(self):
# 創(chuàng)建新線程的時候,Thread 對象會調(diào)用我們的 ThreadFunc 對象踢步,這時會用到一個特殊函數(shù) __call__()癣亚。
print'starting',self.name,'at:',time()
self.res=self.func(*self.args)
printself.name,'finished at:',time()
# writeQ()和 readQ()函數(shù)分別用來把對象放入隊列和消耗隊列中的一個對象。在這里我們使用 字符串'xxx'來表示隊列中的對象获印。
def writeQ(queue):
print'producing object for Q...'
queue.put('xxx',1)
print"size now",queue.qsize()
def readQ(queue):
queue.get(1)
print("consumed object from Q... size now",queue.qsize())
def writer(queue,loops):
# writer()函數(shù)只做一件事述雾,就是一次往隊列中放入一個對象,等待一會兼丰,然后再做同樣的事
foriinrange(loops):
writeQ(queue)
sleep(1)
def reader(queue,loops):
# reader()函數(shù)只做一件事玻孟,就是一次從隊列中取出一個對象,等待一會鳍征,然后再做同樣的事
foriinrange(loops):
readQ(queue)
sleep(randint(2,5))
# 設(shè)置有多少個線程要被運行
funcs=[writer,reader]
nfuncs=range(len(funcs))
def main():
nloops=randint(10,20)
q=Queue(32)
threads=[]
foriinnfuncs:
t=MyThread(funcs[i],(q,nloops),funcs[i].__name__)
threads.append(t)
foriinnfuncs:
threads[i].start()
foriinnfuncs:
threads[i].join()
printthreads[i].getResult()
print'all DONE'
if__name__=='__main__':
main()
FAQ
進程與線程黍翎。線程與進程的區(qū)別是什么?
進程(有時被稱為重量級進程)是程序的一次 執(zhí)行。每個進程都有自己的地址空間艳丛,內(nèi)存匣掸,數(shù)據(jù)棧以及其它記錄其運行軌跡的輔助數(shù)據(jù)趟紊。
線程(有時被稱為輕量級進程)跟進程有些相似,不同的是碰酝,所有的線程運行在同一個進程中霎匈, 共享相同的運行環(huán)境。它們可以想像成是在主進程或“主線程”中并行運行的“迷你進程”送爸。
這篇文章很好的解釋了 線程和進程的區(qū)別铛嘱,推薦閱讀: http://www.ruanyifeng.com/blo…
Python 的線程。在 Python 中袭厂,哪一種多線程的程序表現(xiàn)得更好弄痹,I/O 密集型的還是計算 密集型的?
由于GIL的緣故,對所有面向 I/O 的(會調(diào)用內(nèi)建的操作系統(tǒng) C 代碼的)程序來說嵌器,GIL 會在這個 I/O 調(diào)用之 前被釋放肛真,以允許其它的線程在這個線程等待 I/O 的時候運行。如果某線程并未使用很多 I/O 操作爽航, 它會在自己的時間片內(nèi)一直占用處理器(和 GIL)蚓让。也就是說,I/O 密集型的 Python 程序比計算密集 型的程序更能充分利用多線程環(huán)境的好處讥珍。
線程历极。你認為,多 CPU 的系統(tǒng)與一般的系統(tǒng)有什么大的不同?多線程的程序在這種系統(tǒng)上的表現(xiàn)會怎么樣?
Python的線程就是C語言的一個pthread衷佃,并通過操作系統(tǒng)調(diào)度算法進行調(diào)度(例如linux是CFS)趟卸。為了讓各個線程能夠平均利用CPU時間,python會計算當前已執(zhí)行的微代碼數(shù)量氏义,達到一定閾值后就強制釋放GIL锄列。而這時也會觸發(fā)一次操作系統(tǒng)的線程調(diào)度(當然是否真正進行上下文切換由操作系統(tǒng)自主決定)。