python的GIL

Python 和 GIL

Understanding GIL

GIL

NewGIL

C Python 有一个 全局解释器锁 (GIL)

它有时候会是线程“争用”的来源

它对线程施加了各种限制(您不能使用多个 CPU), 限制了线程性能


什么是线程?

  • Python 线程是真正的系统线程

    POSIX 线程(pthreads)
    
    Windows 线程
    
  • 完全由主机操作系统管理

    所有调度/线程切换

  • Python的线程执行解释器过程(用C编写)


线程创建

Thread类执行 run 方法

Python threads simply execute a "callable"

The run() method of Thread (or a function)

import time
import threading

class CountdownThread(threading.Thread):
	def __init__(self,count):
		threading.Thread.__init__(self)
		self.count = count
		
--> def run(self):
		while self.count > 0:
			print "Counting down", self.count
			self.count -= 1
			time.sleep(5)
		return
		
Python创建一个小型数据结构 包含一些 解释器状态	(interpreter state)	

启动一个新线程(pthread)

线程调用 PyEval_CallObject

最后,运行的 C 函数 调用 指定的 Python 可调用对象


线程特定状态 PyThreadState

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

    • 当前堆栈框架 Current stack frame (for Python code) • 当前递归深度 Current recursion depth • Thread ID • 一些每线程异常信息 Some per-thread exception information • 可选的跟踪/分析/调试 hook Optional tracing/profiling/debugging hooks

  • 它是一个小型 C 结构(<100 字节)

typedef struct _ts {
	struct _ts *next;
	PyInterpreterState *interp;
	struct _frame *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;
} PyThreadState;


线程执行

  • 解释器有一个全局变量,简单地指向当前运行的线程的 ThreadState 结构
/* Python/pystate.c */
...
PyThreadState *_PyThreadState_Current = NULL;
  • 解释器的操作(隐式的)依赖于这个变量,来确定当前执行工作的线程


GIL global interpreter lock

不是一个简单的 互斥锁 mutex lock

它是一个二进制信号量,由 pthreads 互斥量和条件变量构成

	locked = 0 # Lock status
	mutex = pthreads_mutex() # Lock for the status
	cond = pthreads_cond() # Used for waiting/wakeup

GIL 是这个锁的一个实例
release() {
	mutex.acquire()
	locked = 0
	mutex.release()
	cond.signal()
}

acquire() {
	mutex.acquire()
	while (locked) {
	cond.wait(mutex)
	}
	locked = 1
	mutex.release()
}

GIL确保确保每个线程运行时, 都得到对解释器内部的独占访问

线程在运行时持有 GIL, 他们在阻塞 I/O时释放它

所有解释器锁定都基于信号
	要获取 GIL,请检查它是否 free。如果不行,去等待信号
	要释放 GIL,释放它并发出信号

处理 CPU 密集线程,这些线程从不执行任何 I/O,解释器定期执行“检查”

默认情况下,check一次 每 100 个 ticks

The Check Interval 检查间隔 是一个全局计数器,它完全独立于线程的调度


The Periodic Check 定期检查期间会发生什么?

  • 信号处理程序(signal handlers) 将执行待执行的 signals (仅在主线程中)

  • 释放, 获取 GIL

/* Python/ceval.c */
...
if (--_Py_Ticker < 0) {
	 ...
	 _Py_Ticker = _Py_CheckInterval;
	 ...
	 if (things_to_do) {
		 if (Py_MakePendingCalls() < 0) {
		 ...
		 }
	 }
	 if (interpreter_lock) {
	 /* Give another thread a chance */
		 ...
		 PyThread_release_lock(interpreter_lock);
		 /* Other threads may run now */
		 PyThread_acquire_lock(interpreter_lock, 1);
		 ...
	}


什么是 Interpreter Ticks

映射到 解释器

Interpreter ticks 不是基于时间的

x = (i for i in range(100000000))


print(-1 in x)

# Ctrl + C 不能打断 ticks


Signals

为什么 Python线程编程 不能再被 keyboard interrupt 杀死?

必须使用 kill -9 在单独的窗口中关闭

如果 signal 到达,解释器在每次 tick 之后 check, 直到主线程运行

尝试尽可能快地切换线程, 可能希望 main 线程会运行

由于信号处理程序 signal handlers只能在主线程运行,解释器快速 获取/释放 GIL,直到它被调度 所以它只是尝试尽可能快地切换线程, 可能希望 main 线程会运行


Thread Scheduling 线程调度

  • Python 没有线程调度器

  • 没有线程优先级的概念, 抢占、循环调度等

    There is no notion of thread priorities, preemption, round-robin scheduling, etc.

  • 所有线程调度都留给操作系统(例如 Linux、Windows 等)

    这就是信号变得如此奇怪的部分原因 解释器无法控制调度所以 它只是尝试尽可能快地切换线程可能希望 main 会运行


Thread Scheduling 线程调度

  • 信号执行之间的滞后 取决于操作系统

  • 操作系统 会先执行最高“优先级”的线程

    • CPU-bound : low priority • I/O bound : high priority

如果一个信号被发送到一个低优先级的线程,并且CPU忙于更高的优先级任务,它不会运行,直到稍后的时间点


CPU-Bound Threads CPU 密集线程

GIL Battle

两个 CPU-bund 型线程

一个 CPU-bund 一个 I/O -bund

会降低 I/O 绑定线程 的响应时间

因为 I/O 线程不能足够快地醒来以获得 GIL (I/O bund 优先级反转)

只发生在多核

新的 GIL 实现 (python3.2)

only available by svn checkout

不是ticks,现在有一个全局变量 gil_drop_request

/* Python/ceval.c */
...
static volatile int gil_drop_request = 0;

线程一直运行 直到该值设置为 1 , 此时,线程必须丢弃 GIL

新 GIL 图解

Buy me a 肥仔水!