深入學(xué)習(xí)python多線(xiàn)程與GIL
python 多線(xiàn)程效率
在一臺(tái)8核的CentOS上,用python 2.7.6程序執(zhí)行一段CPU密集型的程序。
import time def fun(n):#CPU密集型的程序 while(n>0): n -= 1 start_time = time.time() fun(10000000) print('{} s'.format(time.time() - start_time))#測(cè)量程序執(zhí)行時(shí)間
測(cè)量三次程序的執(zhí)行時(shí)間,平均時(shí)間為0.968370994秒。這就是一個(gè)線(xiàn)程執(zhí)行一次fun(10000000)所需要的時(shí)間。
下面用兩個(gè)線(xiàn)程并行來(lái)跑這段CPU密集型的程序。
import time import threading def fun(n): while(n>0): n -= 1 start_time = time.time() t1 = threading.Thread( target=fun, args=(10000000,) ) t1.start() t2 = threading.Thread( target=fun, args=(10000000,) ) t2.start() t1.join() t2.join() print('{} s'.format(time.time() - start_time))
測(cè)量三次程序的執(zhí)行時(shí)間,平均時(shí)間為2.150056044秒。
為什么在8核的機(jī)器上,多線(xiàn)程執(zhí)行時(shí)間并不比順序執(zhí)行快呢?
再做另一個(gè)實(shí)驗(yàn),用下面的命令,把8核cpu中的7個(gè)核禁掉。
[xxx]# echo 0 > /sys/devices/system/cpu/cpu1/online [xxx]# echo 0 > /sys/devices/system/cpu/cpu2/online [xxx]# echo 0 > /sys/devices/system/cpu/cpu3/online [xxx]# echo 0 > /sys/devices/system/cpu/cpu4/online [xxx]# echo 0 > /sys/devices/system/cpu/cpu5/online [xxx]# echo 0 > /sys/devices/system/cpu/cpu6/online [xxx]# echo 0 > /sys/devices/system/cpu/cpu7/online
然后在運(yùn)行這個(gè)多線(xiàn)程的程序,三次平均時(shí)間為2.533491453秒。為什么多線(xiàn)程程序在多核上跑的時(shí)間只比單核快一點(diǎn)點(diǎn)呢?
這就要提到python程序多線(xiàn)程的實(shí)現(xiàn)機(jī)制了。
Python多線(xiàn)程實(shí)現(xiàn)機(jī)制
python的多線(xiàn)程機(jī)制,就是用C實(shí)現(xiàn)的真實(shí)系統(tǒng)中的線(xiàn)程。線(xiàn)程完全被操作系統(tǒng)控制。
python內(nèi)部創(chuàng)建一個(gè)線(xiàn)程的步驟是這樣的:
- 創(chuàng)建一個(gè)數(shù)據(jù)結(jié)構(gòu)PyThreadState,其中含有一些解釋器狀態(tài)
- 調(diào)用pthread創(chuàng)建線(xiàn)程
- 執(zhí)行線(xiàn)程函數(shù)
由于python是解釋形動(dòng)態(tài)語(yǔ)言,所以在實(shí)現(xiàn)線(xiàn)程時(shí),需要PyThreadState結(jié)構(gòu)來(lái)保存一些信息:
- 當(dāng)前的stack frame (對(duì)python代碼)
- 當(dāng)前的遞歸深度
- 線(xiàn)程ID
- 可選的tracing/profiling/debugging hooks
PyThreadState是C語(yǔ)言實(shí)現(xiàn)的一個(gè)結(jié)構(gòu)體(摘自[2]):
typedef struct _ts { struct _ts *next; # 鏈表指正 PyInterpreterState *interp; # 解釋器狀態(tài) struct _frame *frame; # 當(dāng)前的stack frame int recursion_depth; # 當(dāng)前的遞歸深度 int tracing; int use_tracing; Py_tracefunc c_profilefunc; Py_tracefunc c_tracefunc; PyObject *c_profileobj; PyObject *c_traceobj; PyObject *curexc_type; PyObject *curexc_value; PyObject *curexc_traceback; PyObject *exc_type; PyObject *exc_value; PyObject *exc_traceback; PyObject *dict; int tick_counter; int gilstate_counter; PyObject *async_exc; long thread_id; # 線(xiàn)程ID } PyThreadState;
從目前最新的python源碼中來(lái)看,這個(gè)結(jié)構(gòu)體中的內(nèi)容已經(jīng)有所改變,但記錄解釋器狀態(tài)的指針PyInterpreterState *interp依然存在。
python解釋器實(shí)現(xiàn)時(shí),用了一個(gè)全局變量(_PyThreadState_Current)
[https://github.com/python/cpython/blob/3.1/Python/pystate.c](python3.1和之前的代碼中都存在,python3.2就有所不同了)
PyThreadState *_PyThreadState_Current = NULL;
_PyThreadState_Current指向當(dāng)前執(zhí)行線(xiàn)程的PyThreadState數(shù)據(jù)結(jié)構(gòu)。解釋器通過(guò)這個(gè)變量,來(lái)獲取當(dāng)前所執(zhí)行線(xiàn)程的信息。
python程序中,有一個(gè)全局解釋器鎖GIL來(lái)控制線(xiàn)程的執(zhí)行,每一個(gè)時(shí)刻只允許一個(gè)線(xiàn)程執(zhí)行。
GIL的行為
GIL最基本的行為只有下面兩個(gè):
- 當(dāng)前執(zhí)行的線(xiàn)程持有GIL
- 線(xiàn)程遇到I/O阻塞時(shí),會(huì)釋放GIL。(阻塞等待時(shí),就釋放GIL,給另一個(gè)線(xiàn)程執(zhí)行的機(jī)會(huì))
那么,如果遇到CPU密集型的線(xiàn)程,一直占用CPU,不會(huì)被I/O阻塞,是不是其它線(xiàn)程就沒(méi)有機(jī)會(huì)執(zhí)行了呢?
非也,為了避免這種情況,解釋器還會(huì)周期性的check并執(zhí)行線(xiàn)程調(diào)度。
解釋器周期性check行為,做的就是下面這3件事:
- 復(fù)位tick計(jì)數(shù)器
- 在主線(xiàn)程中,檢查有沒(méi)有需要處理的信號(hào)
- 讓當(dāng)前執(zhí)行線(xiàn)程釋放(Release)GIL,讓其他線(xiàn)程獲取(acquire)GIL并執(zhí)行(給其他線(xiàn)程執(zhí)行的機(jī)會(huì))
而解釋器check的周期,默認(rèn)是100個(gè)tick。解釋器的tick并不是基于時(shí)間的,每個(gè)tick大致相當(dāng)于一條匯編指令的執(zhí)行時(shí)間。
從解釋器的check行為中可以看到,只有主線(xiàn)程中會(huì)處理信號(hào),子線(xiàn)程中都不處理信號(hào)。所以python多線(xiàn)程程序,會(huì)給人一種無(wú)法處理Ctrl+C的假象,因?yàn)榇蟛糠智闆r下主線(xiàn)程被block住了,無(wú)法處理SIGINT信號(hào)。
注意python中并沒(méi)有實(shí)現(xiàn)線(xiàn)程調(diào)度,python的多線(xiàn)程調(diào)度完全依賴(lài)于操作系統(tǒng)。所以python多線(xiàn)程編程中沒(méi)有線(xiàn)程優(yōu)先級(jí)等概念。
GIL的實(shí)現(xiàn)
python的GIL并不是簡(jiǎn)單的用lock實(shí)現(xiàn)的,GIL是用signal實(shí)現(xiàn)的。
- 線(xiàn)程獲取(acquire)GIL前,先檢查有沒(méi)有被free,如果沒(méi)有,就sleep等待signal
- 線(xiàn)程釋放GIL時(shí),還要發(fā)送signal
參考
[1] Understanding the Python GIL. http://dabeaz.com/python/UnderstandingGIL.pdf
[2] Inside the Python GIL. http://www.dabeaz.com/python/GIL.pdf
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
windows下安裝Python虛擬環(huán)境virtualenvwrapper-win
這篇文章主要介紹了windows下安裝Python虛擬環(huán)境virtualenvwrapper-win,內(nèi)容超簡(jiǎn)單,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-06-06關(guān)于Java中RabbitMQ的高級(jí)特性
這篇文章主要介紹了關(guān)于Java中RabbitMQ的高級(jí)特性,MQ全稱(chēng)為Message Queue,即消息隊(duì)列,"消息隊(duì)列"是在消息的傳輸過(guò)程中保存消息的容器,它是典型的:生產(chǎn)者、消費(fèi)者模型,生產(chǎn)者不斷向消息隊(duì)列中生產(chǎn)消息,消費(fèi)者不斷的從隊(duì)列中獲取消息,需要的朋友可以參考下2023-07-07Python操作csv文件之csv.writer()和csv.DictWriter()方法的基本使用
csv文件是一種逗號(hào)分隔的純文本形式存儲(chǔ)的表格數(shù)據(jù),Python內(nèi)置了CSV模塊,可直接通過(guò)該模塊實(shí)現(xiàn)csv文件的讀寫(xiě)操作,下面這篇文章主要給大家介紹了關(guān)于Python操作csv文件之csv.writer()和csv.DictWriter()方法的基本使用,需要的朋友可以參考下2022-09-09python圖像處理-利用一行代碼實(shí)現(xiàn)灰度圖摳圖
這篇文章主要介紹了python圖像處理-利用一行代碼實(shí)現(xiàn)灰度圖摳圖,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05tensorflow: variable的值與variable.read_value()的值區(qū)別詳解
今天小編就為大家分享一篇tensorflow: variable的值與variable.read_value()的值區(qū)別詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-07-07