Python的多线程与GIL
Python从0.9.8版就开始支持多线程( 模块),1.5.1版引入了 高级模块,是对thread模块的封装。
在Python3中, 模块被重命名为 ,强调其作为底层模块应该避免使用。
模块对应源码在 中,我们看下 模块提供的接口函数:
0x00 线程的创建
[1] 创建bootstate结构boot并初始化。
bootstate结构体记录了线程的信息:
, , 记录线程执行的函数以及参数。 和 分别保存了进程状态 对象和线程状态 对象。
通过 获取当前线程状态 对象,进而可以获取进程状态 对象。
定义在 :
通过 函数创建线程状态 对象,定义在 :
[2] 初始化多线程环境
当Python启动时,是并不支持多线程的。换句话说,Python中支持多线程的数据结构以及GIL都是没有创建的,Python之所以有这种行为是因为大多数的Python程序都不需要多线程的支持。Python选择了让用户激活多线程机制的策略。在Python虚拟机启动时,多线程机制并没有被激活,它只支持单线程,一旦用户调用thread.startnewthread,明确指示Python虚拟机创建新的线程,Python就能意识到用户需要多线程的支持,这个时候,Python虚拟机会自动建立多线程机制需要的数据结构、环境以及那个至关重要的GIL。摘自:《Python源码剖析》 — 陈儒
函数定义在 :
[A] 检查是否存在GIL
函数定义在 :
是一组原子操作,封装了不同的平台的原子接口。
定义在 :
函数是c11标准的原子读操作,参考Atomic operations library。
是枚举元素:
指定常规的非原子内存访问如何围绕原子操作排序,这边我们不细究这些原子操作排序的不同,感兴趣可以参考memory order。
我们只关心两种原子操作,一种 原子读和一种 原子写。
是一个 对象,表示Python运行状态对象。 指向 对象,表示Python字节码执行器的运行状态对象,定义在 :
指向 对象,表示GIL的运行状态对象,定义在 :
就是传说中的GIL,是一个 对象,定义在 :
所以, 函数通过原子读操作检查原子位 是否初始化。未初始化时, 为-1。当GIL被线程获取时, 为1,反之为0。
[B] 创建并初始化GIL
结构体中的 和 用来保护 。
The GIL is just a boolean variable (locked) whose access is protectedby a mutex (gilmutex), and whose changes are signalled by a conditionvariable (gilcond). gil_mutex is taken for short periods of time,and therefore mostly uncontended.
GIL利用条件机制和互斥锁 保护一个锁变量 作为实现。
和 定义在 ,根据平台不一样具体实现也不一样,以POSIX Thread(pthread)为例:
函数初始化锁变量 和初始化条件变量 之后,通过 原子写初始化 , 是一个 对象,定义在 :
是一个原子类型指针,当一个线程获得GIL之后, 将指向线程状态 对象。
并通过 原子写初始化 。
[C] 主线程获取GIL
函数定义在 :
首先获取mutex互斥锁,获取mutex互斥锁之后检查是否有线程持有GIL( 为1),如果GIL被占有的话,设置cond信号量等待。等待的时候线程就会挂起,释放mutex互斥锁。cond信号被唤醒时,mutex互斥锁会自动加锁。while语句避免了意外唤醒时条件不满足,大多数情况下都是条件满足被唤醒。
如果等待超时并且这期间没有发生线程切换,就通过 请求持有GIL的线程释放GIL。反之获得GIL就将 置为1,并将当前线程状态 对象保存到 , 切换次数加1。
[D] 检查和初始化原生系统线程环境
是一个 结构对象:
实现了一个机制:
Mechanism whereby asynchronously executing callbacks (e.g. UNIXsignal handlers or Mac I/O completion routines) can schedule callsto a function to be called synchronously.
也就是主线程执行一些被注册的函数, 记录了主线程id:
获得线程id之前会检查 变量,如果说GIL指示着Python的多线程环境是否已经建立,那么这个 变量就指示着为了使用底层平台所提供的原生thread,必须的初始化动作是否完成。
和 定义在 :
POSIX Thread(pthread) 下 会根据编译器和平台来确认是否要进行初始化动作,定义在 :
用来确保线程安全,是一个Python级别的线程锁。
[3] 创建原生系统线程
POSIX Thread(pthread) 下的 定义在 :
函数通过 函数创建原生的系统线程,然后调用 函数将线程状态改为 状态,确保线程运行结束后资源的释放,最后返回线程id。
0x01 线程的执行
传给了 函数用来创建线程的func参数是 函数,arg参数包装了线程信息的boot对象,也就是说主线程创建的子线程将会执行 。
定义在 :
函数先对线程状态对象设置子线程的id,并初始化线程状态对象。
调用 获取GIL,定义在 :
实际上就是调用了 来获取GIL。
获取GIL之后对进程状态对象( )累加线程数,并调用 执行子线程的函数。
在子线程的全部计算完成之后,Python将销毁子线程。
0x02 线程的销毁
清除当前线程对应的线程状态对象,所谓清理,实际上比较简单,就是对线程状态对象中维护的东西进行引用计数的维护。
释放线程状态对象并释放GIL。
在 中,首先会删除清理后的当前线程状态对象,然后通过 释放GIL。
定义在 :
调用了 函数,定义在 :
函数获取mutex互斥锁之后将 置为0,释放GIL,并通知条件变量 。
清除和释放工作完成后,子线程就调用 退出了。
是一个平台相关的操作,完成各个平台上不同的销毁原生线程的工作。在POSIX Thread(pthread) 下,实际上就是调用 函数。
0x03 线程的调度
时间调度
当然,子线程是不会一直执行 到释放GIL,Python中持有GIL的线程会在某个时间释放GIL。
In the GIL-holding thread, the main loop (PyEvalEvalFrameEx) must beable to release the GIL on demand by another thread. A volatile booleanvariable (gildrop_request) is used for that purpose, which is checkedat every turn of the eval loop. That variable is set after a wait of microseconds on has timed out.
[Actually, another volatile boolean variable (eval_breaker) is usedwhich ORs several conditions into one. Volatile booleans aresufficient as inter-thread signalling means since Python is runon cache-coherent architectures only.]
A thread wanting to take the GIL will first let pass a given amount oftime ( microseconds) before setting gildroprequest. Thisencourages a defined switching period, but doesn't enforce it sinceopcodes can take an arbitrary time to execute.
The value is available for the user to read and modifyusing the Python API .
我们看 函数,定义在 :
是虚拟机执行字节码的主入口函数,当满足 时,当前线程会释放GIL,其他等待GIL的线程会尝试获取GIL并执行。
会在 等待超时被设置。而超时时间被设置在 :
会在 初始化的时候会被设置。
默认是0.005s,可以通过sys.getswitchinterval来查看 时间间隔:
阻塞调度
Python3中除了时间调度之外,还有一种阻塞调度:当线程执行I/O操作,或者sleep时,线程将会挂起,释放GIL。
以 为例,实现在 :
函数是跨平台实现,上面只保留非windows平台的代码。
Python在这里使用select来实现sleep阻塞,可以看到select前后有两个宏定义:
定义在 :
和 宏分别调用了 和 函数:
定义在 :
:设置当前线程状态对象为NULL,释放GIL,并保存到 变量。
:获取GIL,重新设置当前线程状态对象。
领取专属 10元无门槛券
私享最新 技术干货