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, args=(10000000,) )
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