
腾讯一面:解释Python中的GIL(全局解释器锁)和它的底层原理?GIL锁和互斥锁的区别是什么?
面试官:请你说说Python中GIL锁的作用和影响?
Python中的GIL(Global Interpreter
Lock,全局解释器锁)是Python解释器(CPython)中用于多线程编程的一个机制。GIL是一个互斥锁,它确保了任何时候只有一个Python线程能够执行Python字节码。这意味着在多线程环境下,虽然操作系统能够同时运行多个线程,但Python的GIL限制了Python代码在同一时间只能被一个线程执行。
为什么需要GIL?
GIL的存在主要是为了简化Python的内存管理。Python的内存管理(如对象分配和垃圾回收)不是线程安全的,如果多个线程能够同时执行Python代码,那么内存管理将变得非常复杂且容易出错。GIL通过确保任何时候只有一个线程能够执行Python代码,从而避免了这种复杂性。
GIL的影响
GIL对Python多线程程序的性能有显著影响:
-
CPU密集型任务 :对于需要大量计算的任务,GIL会成为瓶颈,因为它限制了多线程并行执行的能力。在这种情况下,使用多进程(multiprocessing模块)通常比多线程更有效。
-
I/O密集型任务 :对于涉及大量等待I/O操作(如网络请求、文件读写)的任务,GIL的影响较小。因为当线程等待I/O操作时,它会释放GIL,允许其他线程运行。因此,对于I/O密集型任务,多线程仍然可以带来性能上的好处。
绕过GIL的方法
虽然GIL限制了多线程在某些情况下的性能,但Python程序员仍然有几种方法可以绕过GIL的限制:
-
多进程 :使用multiprocessing模块,可以创建多个独立的Python解释器进程,每个进程都有自己的Python对象和GIL。这样,不同的进程可以并行执行Python代码,绕过单个GIL的限制。
-
C扩展模块 :通过编写C扩展模块,可以释放GIL,并在C代码中执行线程安全的操作。这对于需要高性能且能够处理多线程并行执行的任务特别有用。
-
异步编程 :使用asyncio模块,可以编写异步代码,通过事件循环来管理I/O操作,而不是依赖多线程或多进程。这种方法特别适合于I/O密集型任务。
面试官:说说看GIL锁的底层原理,在多核的环境下,Python多线程能使用到多核吗?
GIL锁的底层原理

上面这张图,就是 GIL 在 Python 程序的工作示例。其中,Thread 1、2、3 轮流执行,每一个线程在开始执行时,都会尝试获取
GIL锁,以阻止别的线程执行,但只有其中一个线程能够获取成功;同样的,每一个线程执行完一段后,会释放 GIL,以允许别的线程开始利用资源。
线程释放GIL锁有两种情况,一是遇到 IO操作 ,二是 Time Tick 到期。
IO操作很好理解,比如线程 发出一个http请求,该线程 等待响应的过程中就会释放GIL锁给其他线程执行 。
那么 Time Tick 到期是什么呢? Time Tick规定了线程的最长执行时间,超过时间后自动释放GIL锁。 Python 3
以后,间隔时间大致为 15毫秒 。这就类似于操作系统管理进程中的时间片概念,只不过 Time Tick
是属于Python解释器自己的时间片。
虽然都是释放GIL锁,但这两种情况是不一样的。比如,Thread1遇到 IO操作
释放GIL,由Thread2和Thread3来竞争这个GIL锁,Thread1 不再参与这次竞争 。
如果是Thread1因为 Time Tick
到期释放GIL(多数是CPU密集型任务),那么三个线程可以同时竞争这把GIL锁,可能出现Thread1在竞争中胜出,再次执行的情况。单核CPU下,这种情况不算特别糟糕。因为只有1个CPU,所以CPU的利用率是很高的。
在多核CPU下,由于GIL锁的全局特性,虽然多线程可能分布在多个核上,但却无法发挥多核的并行特性。

如图所示:一个Python进程中有Thread1 和 Thread2
两个线程在工作。虽然Thread1在CPU1上运行,Thread2在CPU2上运行,但是同一时间只会有1个CPU在为这个Python进程工作。
GIL是全局的,CPU2上的Thread2需要等待CPU1上的Thread1让出GIL锁,才有可能执行。如果在多次竞争中,Thread1都胜出,Thread2没有得到GIL锁,意味着对于该Python进程而言,CPU2一直是闲置的,无法发挥多核的优势。
为了避免同一线程一直霸占CPU,在 python3.2 版本之后,线程会自动的调整自己的优先级,使得多线程任务执行效率更高。
面试官:** **既然GIL锁能保证同一时刻Python进程只有一个线程在执行,那么GIL能否绝对保证线程安全?
GIL不能绝对保证线程安全。 GIL虽然能 确保同一时间只有一个线程能够执行Python字节码。
然而,这种机制主要关注的是Python解释器级别的线程安全,而不是应用程序级别的线程安全。
举个例子:
def add(): global n for i in range(10**1000): n = n +1def sub(): global n for i in range(10**1000): n = n - 1n = 0import threadinga = threading.Thread(target=add,)b = threading.Thread(target=sub,)a.start()b.start()a.join()b.join()print n
上面的程序对n做了同样数量的加法和减法,那么n理论上是0。但运行程序,打印n,发现它不是0。问题出在哪里呢,问题在于python的每行代码不是原子化的操作。比如n
= n+1这步,不是一次性执行的。如果去查看python编译后的字节码执行过程,可以看到如下结果。
19 LOAD_GLOBAL 1 (n)22 LOAD_CONST 3 (1)25 BINARY_ADD 26 STORE_GLOBAL 1 (n)
从过程可以看出, n = n +1 操作分成了四步完成。因此, n = n+1 不是一个原子化操作。
-
1.加载全局变量n
-
2.加载常数1
-
3.进行二进制加法运算
-
4.将运算结果存入变量n。
根据前面的线程释放GIL锁原则,线程a执行这四步的过程中,有可能会让出GIL。如果这样,n=n+1的运算过程就被打乱了。最后的结果中,得到一个非零的n也就不足为奇。
因此,GIL无法保护跨线程的数据共享和竞争条件。如果多个线程同时访问和修改共享数据,而没有适当的同步机制(如锁、信号量等),则可能会导致数据不一致和竞态条件。
面试官:既然说到了同步机制,那说说看GIL锁和互斥锁有什么区别?
线程互斥锁和GIL的区别
1.线程互斥锁是Python代码层面的锁,解决Python程序中多线程共享资源的问题;
2.GIL是Python解释层面的锁,解决解释器中多个线程的竞争资源问题,而GIL锁本质也是一个互斥锁。
欢迎在评论区留言表达看法 或 提出你想学习的技术内容 与 面试问题,阿沛会将其作为往后更新的内容。
如果本文对大家有帮助,麻烦大家动动小手点个免费的“赞”或“在看”,大家的鼓励就是阿沛持续更新的动力~

-- 往期精彩回顾 –
美团一面:你能阐述一下CAP理论的基本概念和核心思想?说说它有哪些分布模型以及如何抉择?