深入學習python多執行緒與GIL
python 多執行緒效率
在一臺8核的CentOS上,用python 2.7.6程式執行一段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))#測量程式執行時間
測量三次程式的執行時間,平均時間為0.968370994秒。這就是一個執行緒執行一次fun(10000000)所需要的時間。
下面用兩個執行緒並行來跑這段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,) ) t2.start() t1.join() t2.join() print('{} s'.format(time.time() - start_time))
測量三次程式的執行時間,平均時間為2.150056044秒。
為什麼在8核的機器上,多執行緒執行時間並不比順序執行快呢?
再做另一個實驗,用下面的命令,把8核cpu中的7個核禁掉。
[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
然後在執行這個多執行緒的程式,三次平均時間為2.533491453秒。為什麼多執行緒程式在多核上跑的時間只比單核快一點點呢?
這就要提到python程式多執行緒的實現機制了。
Python多執行緒實現機制
python的多執行緒機制,就是用C實現的真實系統中的執行緒。執行緒完全被作業系統控制。
python內部建立一個執行緒的步驟是這樣的:
- 建立一個數據結構PyThreadState,其中含有一些直譯器狀態
- 呼叫pthread建立執行緒
- 執行執行緒函式
由於python是解釋形動態語言,所以在實現執行緒時,需要PyThreadState結構來儲存一些資訊:
- 當前的stack frame (對python程式碼)
- 當前的遞迴深度
- 執行緒ID
- 可選的tracing/profiling/debugging hooks
PyThreadState是C語言實現的一個結構體(摘自[2]):
typedef struct _ts { struct _ts *next; # 連結串列指正 PyInterpreterState *interp; # 直譯器狀態 struct _frame *frame; # 當前的stack frame int recursion_depth; # 當前的遞迴深度 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; # 執行緒ID } PyThreadState;
從目前最新的python原始碼中來看,這個結構體中的內容已經有所改變,但記錄直譯器狀態的指標PyInterpreterState *interp依然存在。
python直譯器實現時,用了一個全域性變數(_PyThreadState_Current)
[https://github.com/python/cpython/blob/3.1/Python/pystate.c](python3.1和之前的程式碼中都存在,python3.2就有所不同了)
PyThreadState *_PyThreadState_Current = NULL;
_PyThreadState_Current指向當前執行執行緒的PyThreadState資料結構。直譯器通過這個變數,來獲取當前所執行執行緒的資訊。
python程式中,有一個全域性直譯器鎖GIL來控制執行緒的執行,每一個時刻只允許一個執行緒執行。
GIL的行為
GIL最基本的行為只有下面兩個:
- 當前執行的執行緒持有GIL
- 執行緒遇到I/O阻塞時,會釋放GIL。(阻塞等待時,就釋放GIL,給另一個執行緒執行的機會)
那麼,如果遇到CPU密集型的執行緒,一直佔用CPU,不會被I/O阻塞,是不是其它執行緒就沒有機會執行了呢?
非也,為了避免這種情況,直譯器還會週期性的check並執行執行緒排程。
直譯器週期性check行為,做的就是下面這3件事:
- 復位tick計數器
- 在主執行緒中,檢查有沒有需要處理的訊號
- 讓當前執行執行緒釋放(Release)GIL,讓其他執行緒獲取(acquire)GIL並執行(給其他執行緒執行的機會)
而直譯器check的週期,預設是100個tick。直譯器的tick並不是基於時間的,每個tick大致相當於一條彙編指令的執行時間。
從直譯器的check行為中可以看到,只有主執行緒中會處理訊號,子執行緒中都不處理訊號。所以python多執行緒程式,會給人一種無法處理Ctrl+C的假象,因為大部分情況下主執行緒被block住了,無法處理SIGINT訊號。
注意python中並沒有實現執行緒排程,python的多執行緒排程完全依賴於作業系統。所以python多執行緒程式設計中沒有執行緒優先順序等概念。
GIL的實現
python的GIL並不是簡單的用lock實現的,GIL是用signal實現的。
- 執行緒獲取(acquire)GIL前,先檢查有沒有被free,如果沒有,就sleep等待signal
- 執行緒釋放GIL時,還要傳送signal
參考
[1] Understanding the Python GIL. http://dabeaz.com/python/UnderstandingGIL.pdf
[2] Inside the Python GIL. http://www.dabeaz.com/python/GIL.pdf
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。