深入学习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, 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
参考
[1] Understanding the Python GIL. http://dabeaz.com/python/UnderstandingGIL.pdf
[2] Inside the Python GIL. http://www.dabeaz.com/python/GIL.pdf
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持【听图阁-专注于Python设计】。