Python多线程机制

今天要跟你们一块儿来学习一下Python的多线程机制。有两个缘由,其一是本身在学习中常常会使用到多线程,其二固然是本身对Python中的多线程并非很了解。那么,今天和你们一块儿了解下~python

Python多线程机制 bootstrap

开发多线程的应用系统,是在平常开发中常常会遇到的需求。同时,Python也为多线程系统的开发提供了很好的支持。
你们应该都知道,Python多线程机制是在GIL(Global Interpreter Lock)全局解释锁的基础上创建的。windows

那么Python为何须要全局解释锁?数据结构

为何须要全局解释锁?多线程

咱们知道,要支持多线程的话,一个基本的要求就是不一样线程对共享资源访问的互斥,因此Python中引入了GIL,固然这是第一个缘由。ide

Python中的GIL是一个很是霸道的互斥实现,在一个线程拥有了解释器的访问权以后,其它的全部线程都必须等待它释放解释器的访问权,即便这些线程的下一条指令并不会互相影响。函数

这样的说法也就意味着,不管如何,在同一时间,只能有一个线程能访问Python提供的API。由于单处理器的本质是不可能并行的,这里的同一时间确实对于单处理器是毫无心义的,可是对于多处理器,同一时间,确实能够有多个时间独立运行。然而正是因为GIL限制了这样的情形,使得多处理器最终退化为单处理器,性能大打折扣。那么,为何还要使用GIL呢?这里就要提到第二个缘由。性能

固然,Python社区也早都认识到了这个问题,而且在不断探索,Greg Stein和Mark Hammond两位老兄曾经建立过一份去除GIL的branch,可是很不幸,这个分支在不少的基准测试中,尤为是在单线程的测试上,效率只有使用GIL的一半左右。学习

使用GIL时,保护机制的粒度比较大,也就是咱们彷佛只须要将可能被多个线程共享的资源保护起来便可,对于不会被多个线程共享的资源,彻底能够不用保护。可是,若是使用更细粒度的锁机制进行保护,那么,会致使大量的加锁和解锁功能,加锁和解锁对于操做系统来讲,是一个比较重量级的动做,同时,没有GIL的保护,编写Python的扩展模块的难度也大大增长。测试

因此,目前为止,GIL仍然是多线程机制的基石。

对于Python而言,字节码解释器是Python的核心所在,因此Python经过GIL来互斥不一样线程对解释器的使用。这里举个例子进行说明:

假设,如今有三个线程A、B和C,它们都须要解释器来执行字节码,进行对应的计算,那么在这以前,它们必须得到GIL。那么如今假设线程A得到了GIL,其它线程只能等A释放GIL以后,才能得到。

对!是这样没错,因而,有两个问题:

1. 线程A什么时候释放GIL呢(若是A使用完解释器以后才释放GIL,那么,并行的计算退化为串行,多线程的意义何在?)

2. 线程B和C谁将在A释放GIL以后得到GIL呢?

因此毫无疑问的,Python拥有其本身的一套线程调度机制。

关于线程调度

和操做系统的进程调度同样,线程调度机制主要解决两个问题:

1. 在什么时候挂起当前线程,选择处于等待状态的下一个线程?

2. 在众多处于等待状态的线程中,应该选择激活哪一个线程?

对于什么时候进行线程调度的问题,是由Python自身决定的。咱们能够联想操做系统进行进程切换的问题,当一个进程执行了一段时间以后,发生了时钟中断,因而操做系统响应时钟中断,并在这时开始进程的调度。

与此相似,Python中经过软件模拟了这样的中断,来激活线程的调度。Python的字节码解释器是按照指令的顺序一条一条的顺序执行从而工做的,Python内部维护着这样一个数值,做为Python内部的时钟,假设这个值为N,那么Python将在执行了N条指令以后马上启动线程调度机制。

也就是说,当一个线程得到GIL后,Python内部的监测机制就开始启动,当这个线程执行了N条指令后,Python解释器将强制挂起当前线程,开始切换到下一个处于等待状态的线程。

在Python中,能够这样得到这个数值(N):


那么,下一个问题,Python会在众多等待的线程中选择哪个呢?

答案是,不知道。由于这个问题是交给了底层的操做系统来解决的,Python借用了底层操做系统所提供的线程调度机制来决定下一个得到GIL进入解释器的线程是谁。

因此说,Python中的线程实际上就是操做系统所支持的原生线程。

那么,接下来,咱们一块儿揭开Python中GIL的真实面目。

关于GIL

应该知道,Python中多线程经常使用的两个模块:Thread和在其之上的threading。其中Thread是使用C实现的,而Threading是用python实现。

咱们能够经过Thread模块进行分析(以Python2.7.13为例)。

建立线程
首先从建立线程提及,在threadmodule.c中,thread_PyThread_start_new_thread()函数经过三个主要的动做完成一个线程的建立:

//建立bootstate结构
boot = PyMem_NEW(struct bootstate, 1); if (boot == NULL) return PyErr_NoMemory(); boot->interp = PyThreadState_GET()->interp; boot->func = func; boot->args = args; boot->keyw = keyw; boot->tstate = _PyThreadState_Prealloc(boot->interp); if (boot->tstate == NULL) { PyMem_DEL(boot); return PyErr_NoMemory(); } Py_INCREF(func); Py_INCREF(args); Py_XINCREF(keyw); // 初始化多线程环境
PyEval_InitThreads(); //建立线程
ident = PyThread_start_new_thread(t_bootstrap, (void*) boot); if (ident == -1) { PyErr_SetString(ThreadError, "can't start new thread"); Py_DECREF(func); Py_DECREF(args); Py_XDECREF(keyw); PyThreadState_Clear(boot->tstate); PyMem_DEL(boot); return NULL; } return PyInt_FromLong(ident);

1. 建立并初始化bootstate结构boot,在boot中,将保存关于Python的一切信息(线程过程,线程过程参数等)。

2. 初始化Python的多线程环境。

3. 以boot为参数,建立操做系统的原生线程。

从以上代码能够看出,Python在刚启动时,并不支持多线程,也就是说,Python中支持多线程的数据结构以及GIL都是没有建立的。固然这是由于大多数的Python程序都不须要Python的支持。

在Python虚拟机启动时,多线程机制并无被激活,它只支持单线程,一旦用户调用thread.start_new_thread,明确的告诉Python虚拟机须要建立新的线程,这时Python意识到用户须要多线程的支持,这个时候,Python虚拟机会自动创建多线程须要的数据结构、环境以及GIL。

创建多线程环境

创建多线程环境,主要就是建立GIL。那么GIL是如何实现的呢?
打开"python/ceval.c":

static PyThread_type_lock interpreter_lock = 0; /* This is the GIL */
static PyThread_type_lock pending_lock = 0; /* for pending calls */
static long main_thread = 0; int PyEval_ThreadsInitialized(void) { return interpreter_lock != 0; } void PyEval_InitThreads(void) { if (interpreter_lock) return; interpreter_lock = PyThread_allocate_lock(); PyThread_acquire_lock(interpreter_lock, 1); main_thread = PyThread_get_thread_ident(); }

在这段代码中,iterpreter_lock就是GIL。

不管建立多少个线程,Python创建多线程环境的动做只会执行一次。在建立GIL以前,Python会检查GIL是否已经被建立,若是是,则再也不进行任何动做,不然,就会去建立这个GIL。

在上述代码中,咱们能够看到,建立GIL使用的是Pythread_allocate_lock完成的,下面看看该函数的内部实现:

PyThread_type_lock PyThread_allocate_lock(void) { PNRMUTEX aLock; dprintf(("PyThread_allocate_lock called\n")); if (!initialized) PyThread_init_thread(); aLock = AllocNonRecursiveMutex() ; dprintf(("%ld: PyThread_allocate_lock() -> %p\n", PyThread_get_thread_ident(), aLock)); return (PyThread_type_lock) aLock; }

能够看到该函数返回了alock,alock是结构体PNRMUTEX,实际上就是咱们须要建立的那个interperter_lock(GIL)。这么说来,GIL就是结构体PNRMUTEX呀,因而咱们找来它的真身:

typedef struct NRMUTEX { LONG owned ; DWORD thread_id ; HANDLE hevent ; } NRMUTEX, *PNRMUTEX ;

这里又三个变量,owned、thread_id和hevent。这里的hevent是windows平台下的Event这个内核对象,也就是经过Event来实现线程之间的互斥。thread_id将记录任一时刻得到GIL的线程的id。

那么owned是什么呢?

GIL中的owned是指示GIL是否可用的变量,它的值被初始化为-1,Python会检查这个值是否为1,若是是,则意味着GIL可用,必须将其置为0,当owned为0后,表示该GIL已经被一个线程占用,不可再用;同时,当一个线程开始等待GIL时,其owned就会被增长1;当一个线程最终释放GIL时,必定会将GIL的owned减1,这样,当全部须要GIL的线程都最终释放了GIL以后,owned将再次变为-1,意味着GIL再次变为可用。

 

关于Python中的多线程,今天咱们就学到这里。

相关文章
相关标签/搜索