关键词:GIL、Thread、Ticks、Check、Signal。

主要介绍python多线程和GIL的一些东西。

总结:

•CPython的线程是操作系统的原生线程。在Linux上为pthread,在Windows上为Win thread,完全由操作系统调度线程的执行。一个Python解释器进程内有一个主线程,以及多个用户程序的执行线程。即便使用多核心CPU平台,由于GIL的存在,也将禁止多线程的并行执行。[2]

· Python解释器进程内的多线程是以协作多任务方式执行。当一个线程遇到I/O任务时,将释放GIL。计算密集型(CPU-bound)的线程在执行大约100次解释器的计步(ticks)时,将释放GIL。计步(ticks)可粗略看作Python虚拟机的指令。计步实际上与时间片长度无关。可以通过sys.setcheckinterval()设置计步长度。

· 在单核CPU上,数百次的间隔检查才会导致一次线程切换。在多核CPU上,存在严重的线程颠簸(thrashing)。

· Python 3.2开始使用新的GIL。新的GIL实现中用一个固定的超时时间来指示当前的线程放弃全局锁。在当前线程保持这个锁,且其他线程请求这个锁时,当前线程就会在5毫秒后被强制释放该锁。

· 可以创建独立的进程来实现并行化。Python 2.6引进了多进程包multiprocessing。或者将关键组件用C/C++编写为Python扩展,通过ctypes使Python程序直接调用C语言编译动态链接库的导出函数。

一些细节展开:

Part 1:什么是GIL

python是一种解释型语言,执行前不需要像C/C++提前编译生成CPU能直接执行的指令,python的编译和执行是同时进行的,即一边编译一边执行。这也就要求python在执行时必须要有一个python解释器,把python写成的代码翻译成对应的指令,然后送到CPU执行。这也是python、java等脚本语言不如C/C++等编译型语言快速的原因。

GIL(Global Interpreter Lock)即全局解释器锁,存在于python解释器中,用来确保当前只有一个线程被执行。线程t1在python解释器中执行时会获得GIL,退出时释放GIL。GIL的存在让Python无法充分利用CPU资源,同时不能实现真正意义上的多线程编程。

当有一个线程t1获得GIL执行时,python不允许在同一时间在执行另一个t2线程,即使你是2核4线程或者更多。python不允许线程的并行执行。即使你通过threading包的Thread函数进行了多线程操作,在真正执行的时候,是按串型的方式执行的,即一个线程释放GIL另一个线程获得GIL开始执行。(针对多核心CPU)

GIL的存在保证了只有正在执行的线程才可以与解释器的内核进行通信,避免了混乱。

part2: GIL的工作方式

(1)线程执行时持有GIL。发生I/O请求时释放GIL同时线程处于wait状态,然后按照OS制定的线程schedul执行下一个ready的线程,构成了一个合作的多任务模式。

(2)对于计算密集型的进程(没有I/O操作),解释器会周期性(100 ticks)的做一个check。可以通过sys.setcheckinterval()设置ticks的值。

注意:check的间隔是一个完全独立于线程调度的全局计数器。下面的讨论ticks的值默认是100.

Check 时发生了什么?

(1)如果check发生时是在主线程中,signal handlers会执行处于等待状态的signals。(很快速的执行)

(2)释放和快速获取GIL。

其中,第二条解释了多CPU计算密集型的线程是如何运行的(通过简单的释放GIL,然后其他的线程获得执行的机会)

下一个问题:什么是Tick?解释器的Tick是与时间无关的。TIck是不能被Crtl-C signal终止的。

tick 可以粗略的认为是解释器的指令。100ticks即解释器执行了100记步的指令。

在前面有提到过,只有在main-thread执行时,signal才会被处理。所以在python在进行多线程运算时,如果运行在子线程中,来自键盘的信号Crtl-C是没有作用的,也即不能通过Ctrl-C马上终止正在运行的线程。

但是接收到的信号不会被置之不理,而是在等待状态。当接收到Crtl-C信号后,check不再是100ticks发生一次,而是每一个tick后都会进行一次check,但是由于signal handlers 只能运行在主线程中,所以解释器必须每次tick后都做一次check,解释器快速的释放和重新获取GIL,直到发生线程切换,并且是主线程获得GIL然后running。

一般情况下,当你对一个thread执行了join后,这个线程执行时是不能Ctrl-C直接退出的,因为此时的主线程处于冻结状态。

由于python没有设置线程优先级、scheduling等权限,所以当有signal被接收时,解释器只能通过快速的check期望尽早进入主线程执行signal,由于执行的check数较多,这大大拖慢了线程的执行速度。

此外,GIL不是一个简单的互斥锁,并且所有的解释器的锁都是基于信号的。

(1)为了获得GIL,要检查它是否free,如果是lock则选择sleep并且等待一个唤醒的信号。

(2)释放GIL同时给出信号。

 

Par3:   python的线程-Thread

python的线程是操纵系统的原生线程。线程的schedul和switch完全由操作系统决定,也就是说python解释器不能决定线程的执行优先级以及切换进程等。

UNIX/LINUX:POSIX threads(pthreads)

Windows threads 

也就是说在UNIX/LINUX下是pthreads,在Windows下是winthreads。

python线程的生成:生成一个小的数据结构保存python解释器的一些基本状态信息。一个新的线程被启动。这个新的线程调用PyEval_CallObjiect。

最后一步调用的是一个C函数,用来执行指定的Python的任何一个可调用函数。

Thread-Specific State(线程的特定状态):

(1)每一个线程都有自己的特定的解释器数据结构(PyThreadState)

            目前占有的栈的框架(frame)

            目前的递归深度

            线程的ID

           先前一些线程的异常信息

           可供选择的跟踪、编译、调试工具

(2)它是小于100bytes 的C 结构体

线程的执行:

(1)解释器会定义一个全局变量指向正在运行的线程的状态结构体(ThreadState structure)

(2)解释器只能通过这个变量才知道它正在执行哪个线程。

线程调度:

只有操作系统有权限确定线程的优先级,通常请况下:CPU-bound(计算密集型)优先级低,I/Obound(I/O密集型)优先级高。因为,发生I/O请求时通常伴随着线程的切换,所以可以更好的利用CPU的计算资源,而CPU-bound则会长时间占用CPU的计算资源,导致其他任务只能等待。

如果CPU在执行一个优先级较高的任务时接收到了一个低优先级的线程执行信号,那这个低优先级的线程会被挂起并在合适的时候执行,比如发生了I/O请求,以及check等。

理论上,如果CPU正在执行一个低优先级的任务,接收到了一个高优先级的请求信号,CPU会挂起低优先级的任务,转而执行较高优先级的任务。但是对python来说,由于GIL的存在,尤其是在多核处理器中,事情并不是这么简单。我们会在下面讨论。

轻松一刻:运行一个简单的多线程程序。

from threading import Thread
import time

def count(n):
    while n>0:
        n-=1


if __name__=="__main__":
    t0=time.time()
    count(100000000)
    print time.time()-t0
    t11=time.time()
    t1=Thread(target=count,args=(100000000,))
    t1.start()
    t2=Thread(target=count,args=(100000000,))
    t2.start()
    t1.join()
    t2.join()
    print time.time()-t11

不用多线程时运行时间大概是3s,调用多线程时运行时间是10s。当然,具体时间和操作系统硬件配置有关,但是基本上如果使用python的多线程,都会是这个结果。本来是为了提高执行效率,结果却相反。那么是什么导致了这个结果呢?

在Part2提到过,python解释器的多线程执行是基于GIL state的signal。当一个线程在执行时,GIL是处于Unfree状态,所以发生的另一个请求在侦测到这个信号后只能处于等待的状态,直到获得GIL的释放信号,才有可能被执行。所以,即使你调用了多线程,也只能等待当前线程执行完成而不能同时执行多个线程即使是多核处理器。而由于另一个线程总在等待一个可以执行的信号,所以需要额外的pthreads处理和系统调用资源来传递这些signal,效率自然不如单线程。

你可能会问,如果我的CPU有两个核心,为什么不能同时执行两个线程呢?理论上你用其他语言是可以的,但是python不行,因为GIL只有一个,只存在于一个python解释器中,不能有多个。没错,你有两个核心,在执行计算密集型任务时,操作系统决定在两个核心上同时执行这个任务,但是当一个线程启动时,GIL处于lock,另一个线程获取不到GIL,只能等待,不会被执行。这个时候冲突就产生了,python只想运行单线程,但是操作系统觉得我有权利充分利用多线程资源。

另一个问题就是优先级的反转。一个CPU-bound线程(low priority)在执行时会阻碍一个I/O-bound线程(high priority)。

这是因为I/O线程不能在CPU-bound线程之间及时的获得GIL,这种情况只存在于多核处理器中。

                                                                                         图一 多核处理中的优先级反转

 

有没有好的方法解决这个问题呢?

(1)开发python线程调度工具,由python自行决定线程执行的优先级,或者是一个和操作系统合作的模式。

(2)在操作系统调度器,解释器实现,线程库和C扩展模块之间引入一个新的non-trivial interaction。这个怕翻译不准,就直接用原文吧。字面意思是引入一个不平凡的交互器。

这些做法是很有必要的,可以带来一下几个好处:

(1)线程的执行更可控和可预测,同时减少资源的消耗

(2)可以调高包含多CPU多I/O进程的执行效率。

(3)对那些跟线程有关的库很友好。(multiprocessing)

(4)不用重写整个python解释器。

 

最后,欢迎大家和我交流讨论。

Logo

更多推荐