1. 程式人生 > 程式設計 >深入學習python多執行緒與GIL

深入學習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

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。