一、 背景 之前写过一篇笔记,从理论上学了了GIL解释器锁,文章地址:深入探究GIL的利与弊 。当时使用的Python版本比较旧,可能与当前文章中部分源码说明有出入。
今天通过深挖源码,学习下Python底层是如何实现GIL解释器锁,分析下GIL 解释器锁的构造与应用, 探寻为什么这把GIL锁锁的是字节码。本篇笔记使用的Python版本为3.11.0 alpha 0。
二、GIL锁的构造 1.GIL结构体位于/Include/internal/pycore_gil.h头文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct _gil_runtime_state { unsigned long interval; _Py_atomic_address last_holder; _Py_atomic_int locked; unsigned long switch_number; PyCOND_T cond; PyMUTEX_T mutex; #ifdef FORCE_SWITCHING PyCOND_T switch_cond; PyMUTEX_T switch_mutex;#endif };
说明:
(1). interval: 每一个Python的API所占用锁的时间,单位为微秒。
(2). last_holder: 用于记录最后持有该锁的线程,这样便于检测锁是否被其他线程拿到,然后重新调度。
(3). locked: 标识GIL解释器锁是否被某个线程拿到, 其他线程可以通过locked字段用于判断是否具备执行权或是阻塞
(4). cond: 条件变量,用于阻塞其他线程直至GIL解释器锁被释放的条件满足,常用于和mutex一起使用,实现互斥同步访问。
(5). mutex: 互斥变量, 用于确保对变量的修改同步执行。
(6). switch_cond: 等待锁重新被其他线程拿到的条件变量。
(7). switch_mutex: 用于阻塞释放锁的进程直到任意等待的线程拿到GIL, 并上处理机执行。
(8). switc_number: 用于记录运行过程中GIL解释器锁切换到不同执行线程的次数。
2.PyCOND_T结构体如下:
1 2 3 4 5 6 typedef struct _PyCOND_T { HANDLE sem; int waiting; } PyCOND_T;
3.typedef CRITICAL_SECTION PyMUTEX_T;
PyMUTEX_T是一个线程锁, 用于确保多线程中在临界区的互斥执行。
4.从上面的数据结构可以看到,Python底层的这把GIL解释器锁通过条件变量+”互斥量”来实现多线程中的同步互斥执行,其中的互斥量不再简单是大学操作系统书上的0或者1,而是使用CRITICAL_SECTION线程锁来充当””互斥量”, 与此同时,操作系统书上的mutex的0/1值在这里也就对应上加锁与解锁了。
5.条件变量的初始化和释放以及互斥量的初始化,加锁,解锁和释放的源码位于/Python/condvar.h头文件中,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 Py_LOCAL_INLINE(int ) PyMUTEX_INIT(PyMUTEX_T * cs ) { InitializeCriticalSection(cs ) ; return 0 ; }Py_LOCAL_INLINE(int ) PyMUTEX_FINI(PyMUTEX_T * cs ) { DeleteCriticalSection(cs ) ; return 0 ; }Py_LOCAL_INLINE(int ) PyMUTEX_LOCK(PyMUTEX_T * cs ) { EnterCriticalSection(cs ) ; return 0 ; }Py_LOCAL_INLINE(int ) PyMUTEX_UNLOCK(PyMUTEX_T * cs ) { LeaveCriticalSection(cs ) ; return 0 ; }Py_LOCAL_INLINE(int ) PyCOND_INIT(PyCOND_T * cv ) { cv->sem = CreateSemaphore(NULL, 0, 100000, NULL) ; if (cv->sem==NULL) return -1 ; cv->waiting = 0 ; return 0 ; }Py_LOCAL_INLINE(int ) PyCOND_FINI(PyCOND_T * cv ) { return CloseHandle(cv ->sem ) ? 0 : -1 ; }
三、 GIL锁涉及的操作集 接下来简要分析GIL相关的操作集,其实看到了这里,知道了GIL锁实际上就是由条件变量+互斥变量构造出的一把线程锁,当启动一个Python解释器,会创建一个主线程,当只有一个主线程,加不加锁并不会降太多速度,只是在按行执行每条字节码时增加了加锁和解锁的开销罢了。但是当在一个解释器进程中开辟多个线程,那么由于存在线程锁,在不会主动释放执行权的CPU密集型任务中,只能够通过执行一定量的字节码或者执行一段时间片,被迫释放执行权,由操作系统重新调度其他线程执行,这种情况下,多线程在单核CPU上由于锁的存在,被迫同步互斥执行,执行效率远不及真正的多线程并发执行,执行效率甚至可能还不如单线程,毕竟在Windows上来说线程的上下文切换要在内核完成,因此少不了内核态和用户态的频繁相互切换。
GIL锁相关的宏定义源码位于:/Python/ceval_gil.h中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #define MUTEX_INIT(mut) \ if (PyMUTEX_INIT(&(mut))) { \ Py_FatalError("PyMUTEX_INIT(" #mut ") failed" ); }; #define MUTEX_FINI(mut) \ if (PyMUTEX_FINI(&(mut))) { \ Py_FatalError("PyMUTEX_FINI(" #mut ") failed" ); }; #define MUTEX_LOCK(mut) \ if (PyMUTEX_LOCK(&(mut))) { \ Py_FatalError("PyMUTEX_LOCK(" #mut ") failed" ); }; #define MUTEX_UNLOCK(mut) \ if (PyMUTEX_UNLOCK(&(mut))) { \ Py_FatalError("PyMUTEX_UNLOCK(" #mut ") failed" ); }; #define COND_INIT(cond) \ if (PyCOND_INIT(&(cond))) { \ Py_FatalError("PyCOND_INIT(" #cond ") failed" ); }; #define COND_SIGNAL(cond) \ if (PyCOND_SIGNAL(&(cond))) { \ Py_FatalError("PyCOND_SIGNAL(" #cond ") failed" ); }; #define COND_WAIT(cond, mut) \ if (PyCOND_WAIT(&(cond), &(mut))) { \ Py_FatalError("PyCOND_WAIT(" #cond ") failed" ); }; #define COND_TIMED_WAIT(cond, mut, microseconds, timeout_result) \ { \ int r = PyCOND_TIMEDWAIT(&(cond), &(mut), (microseconds)); \ if (r < 0 ) \ Py_FatalError("PyCOND_WAIT(" #cond ") failed" ); \ if (r) \ timeout_result = 1 ; \ else \ timeout_result = 0 ; \ } \
说明:
1.关于锁的基本操作,C将其设定为宏定义,在一定程度上可以提高运行效率,涉及到的操作位于第二节的第3点。
四、总结 1.因为这把GIL锁的存在,Python的多线程无法充分在单核或多核上充分利用CPU资源,不过在遇到I/O事件前,线程会释放GIL锁。当然,我们可以使用多进程或者协程来在某些场景下替代多线程。ceval_gil.h头文件中还包含了一些操作集,这里就不分析了,有兴趣的朋友可以自行阅读源码,毕竟这把GIL设计在今天看来并不是Python的亮点之一。
2.最后回答下开篇提到的一个问题:为什么这把GIL锁锁的是字节码?,在Python程序执行前,解释器会将代码解释成字节码,然后Python VM将逐行读取字节码指令,获取操作码和操作数,根据操作码执行不同的函数。
3.在Python3.11.0版本中,每个线程默认占有GIL解释器锁的时间为5ms。涉及代码如下:
1 2 3 4 5 6 7 8 9 #define DEFAULT_INTERVAL 5000 static void _gil_initialize(struct _gil_runtime_state *gil) { _Py_atomic_int uninitialized = {-1 }; gil->locked = uninitialized; gil->interval = DEFAULT_INTERVAL; }
1 2 3 4 5 6 Py_LOCAL_INLINE(int ) PyCOND_TIMEDWAIT(PyCOND_T *cv, PyMUTEX_T *cs, long long us) { return _PyCOND_WAIT_MS(cv, cs, (DWORD)(us/1000 )); }
1 2 3 4 5 6 7 8 9 10 11 #define COND_TIMED_WAIT(cond, mut, microseconds, timeout_result) \ { \ int r = PyCOND_TIMEDWAIT(&(cond), &(mut), (microseconds)); \ if (r < 0 ) \ Py_FatalError("PyCOND_WAIT(" #cond ") failed" ); \ if (r) \ timeout_result = 1 ; \ else \ timeout_result = 0 ; \ } \
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Py_LOCAL_INLINE(int ) _PyCOND_WAIT_MS(PyCOND_T *cv, PyMUTEX_T *cs, DWORD ms) { DWORD wait; cv->waiting++; PyMUTEX_UNLOCK(cs); wait = WaitForSingleObjectEx(cv->sem, ms, FALSE); PyMUTEX_LOCK(cs); if (wait != WAIT_OBJECT_0) --cv->waiting; if (wait == WAIT_FAILED) return -1 ; return wait != WAIT_OBJECT_0; }