python 多線程效率
在一臺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))#測量程序執(zhí)行時間
測量三次程序的執(zhí)行時間,平均時間為0.968370994秒。這就是一個線程執(zhí)行一次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, args=(10000000,) ) t2.start() t1.join() t2.join() print('{} s'.format(time.time() - start_time))
測量三次程序的執(zhí)行時間,平均時間為2.150056044秒。
為什么在8核的機器上,多線程執(zhí)行時間并不比順序執(zhí)行快呢?
再做另一個實驗,用下面的命令,把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程序多線程的實現(xiàn)機制了。
Python多線程實現(xiàn)機制
python的多線程機制,就是用C實現(xiàn)的真實系統(tǒng)中的線程。線程完全被操作系統(tǒng)控制。
python內(nèi)部創(chuàng)建一個線程的步驟是這樣的:
- 創(chuàng)建一個數(shù)據(jù)結(jié)構(gòu)PyThreadState,其中含有一些解釋器狀態(tài)
- 調(diào)用pthread創(chuàng)建線程
- 執(zhí)行線程函數(shù)
由于python是解釋形動態(tài)語言,所以在實現(xiàn)線程時,需要PyThreadState結(jié)構(gòu)來保存一些信息:
- 當前的stack frame (對python代碼)
- 當前的遞歸深度
- 線程ID
- 可選的tracing/profiling/debugging hooks
PyThreadState是C語言實現(xiàn)的一個結(jié)構(gòu)體(摘自[2]):
typedef struct _ts { struct _ts *next; # 鏈表指正 PyInterpreterState *interp; # 解釋器狀態(tài) 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源碼中來看,這個結(jié)構(gòu)體中的內(nèi)容已經(jīng)有所改變,但記錄解釋器狀態(tài)的指針PyInterpreterState *interp依然存在。
python解釋器實現(xiàn)時,用了一個全局變量(_PyThreadState_Current)
[https://github.com/python/cpython/blob/3.1/Python/pystate.c](python3.1和之前的代碼中都存在,python3.2就有所不同了)
PyThreadState *_PyThreadState_Current = NULL;
_PyThreadState_Current指向當前執(zhí)行線程的PyThreadState數(shù)據(jù)結(jié)構(gòu)。解釋器通過這個變量,來獲取當前所執(zhí)行線程的信息。
python程序中,有一個全局解釋器鎖GIL來控制線程的執(zhí)行,每一個時刻只允許一個線程執(zhí)行。
GIL的行為
GIL最基本的行為只有下面兩個:
- 當前執(zhí)行的線程持有GIL
- 線程遇到I/O阻塞時,會釋放GIL。(阻塞等待時,就釋放GIL,給另一個線程執(zhí)行的機會)
那么,如果遇到CPU密集型的線程,一直占用CPU,不會被I/O阻塞,是不是其它線程就沒有機會執(zhí)行了呢?
非也,為了避免這種情況,解釋器還會周期性的check并執(zhí)行線程調(diào)度。
解釋器周期性check行為,做的就是下面這3件事:
- 復(fù)位tick計數(shù)器
- 在主線程中,檢查有沒有需要處理的信號
- 讓當前執(zhí)行線程釋放(Release)GIL,讓其他線程獲取(acquire)GIL并執(zhí)行(給其他線程執(zhí)行的機會)
而解釋器check的周期,默認是100個tick。解釋器的tick并不是基于時間的,每個tick大致相當于一條匯編指令的執(zhí)行時間。
從解釋器的check行為中可以看到,只有主線程中會處理信號,子線程中都不處理信號。所以python多線程程序,會給人一種無法處理Ctrl+C的假象,因為大部分情況下主線程被block住了,無法處理SIGINT信號。
注意python中并沒有實現(xiàn)線程調(diào)度,python的多線程調(diào)度完全依賴于操作系統(tǒng)。所以python多線程編程中沒有線程優(yōu)先級等概念。
GIL的實現(xiàn)
python的GIL并不是簡單的用lock實現(xiàn)的,GIL是用signal實現(xiàn)的。
- 線程獲取(acquire)GIL前,先檢查有沒有被free,如果沒有,就sleep等待signal
- 線程釋放GIL時,還要發(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
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
