当前位置:首页>python>Python快速入门学习笔记三十七:GIL 全局解释器锁

Python快速入门学习笔记三十七:GIL 全局解释器锁

  • 2026-10-11 05:51:30
Python快速入门学习笔记三十七:GIL 全局解释器锁
❝

Python入门第三十七课,主要是学习了解了全局解释器锁GIL,GIL是CPython的全局锁,确保同一时刻只有一个线程执行字节码,简化内存管理但限制了多线程的CPU并行能力。

  • 一、GIL是什么?
  • 二、为什么 CPython 要有 GIL?
  • 三、GIL 如何工作?
  • 四、如何绕过 GIL 的限制?
  • 五、GIL 对比 Lock/RLock

一、GIL是什么?

全称:Global Interpreter Lock(全局解释器锁)。

概念:GIL 是 CPython 解释器中的一把互斥锁。

作用:无论 CPU 有多少个核心,在某一时刻,只允许同一个进程中的一个线程去执行 Python 代码,保护 Python 对象内部状态不被并发修改,简化内存管理(尤其是引用计数)。

范围:每个 Python 进程拥有一个独立的GIL(多进程不受影响)。

实现:CPython 核心的一个互斥锁,线程在运行 Python 代码前必须获得 GIL,执行完毕后释放(或主动让出)。

二、为什么 CPython 要有 GIL?

1、简化内存管理(核心原因)

Python 使用引用计数来管理内存。每个对象都有一个计数器,记录被引用的次数。如果没有GIL,多个线程可能同时修改同一个对象的引用计数,导致计数错误、内存泄露或重复释放。

2、避免复杂的细粒度锁

如果不使用 GIL,解释器需要为每个对象(整数、列表、字典等)加锁,这会造成:

  • 巨大的性能开销(锁的获取和释放)
  • 死锁风险增加
  • 实现极其复杂(几乎无法维护)

3、历史原因

Python 早期设计(1990年代)时多核 CPU 并不普及,选择 GIL 是一种务实且高效的工程决策。时至今日,许多 C 扩展库(如 NumPy)依赖 GIL 的简洁性,完全移除 GIL 将破坏大量现有代码。

4、如果没有 GIL 锁,那么 Python 底层就可能会出现引用计数错误,导致内存“爆炸”。

三、GIL 如何工作?

基本流程

⒈线程启动时,会尝试获取 GIL。

⒉获得 GIL 的线程开始执行 Python 字节码。

⒊以下情况会释放 GIL:

  • 线程执行I/O操作(如time.sleep()、socket.recv()、file.read())时,会主动释放。
  • 线程执行了固定数量的字节码指令(默认100条,可通过sys.setswitchinterval()调整时间间隔),会强制释放。
  • 线程执行完毕。

⒋释放GIL 后,操作系统会调度另一个等待的线程获得 GIL。

关键点

  • I/O操作:Python 大多数 I/O 函数会主动释放GIL,因此多线程在 I/O 密集型场景下能真正并发(因为等待 I/O 时其他线程可以运行)。
  • CPU 密集型操作:纯 Python 计算不会主动释放 GIL,只能靠时间片抢占。但 GIL 的存在使得多个 CPU 核心无法同时执行 Python 字节码,因此多线程 CPU 任务反而可能因锁竞争变慢。

四、如何绕过 GIL 的限制?

1、针对 CPU 密集型任务:

多进程替代多线程:

  • 使用 multiprocessing模块创建独立进程,每个进程有独立的 GIL 和内存空间,可真正并行利用多核。
  • 使用场景:科学计算、批量数据处理。

调用C/C++扩展:

  • 在NumPy、Numba等库中,底层计算会主动释放 GIL,使多线程能并行执行(如矩阵运算)。

2、针对 I/O 密集型任务:

线程池优化:

  • 使用concurrent.futures.ThreadPoolExecutor管理线程,避免手动创建过多线程导致切换开销。

异步编程(asyncio):

  • 通过事件循环在单线程内高效调度I/O操作,避免线程切换成本,适合高并发网络服务。

五、GIL 对比 Lock/RLock

结论:GIL 为了确保 CPython 解释器级别的数据安全,作为日常编码来说,我们对 GIL 是无感的,但对于Lock/RLock是实际编码中使用较多的,Lock/RLock是为了确保业务逻辑的完整。下面介绍两个Lock/RLock的示例。

示例一:让打印是完整的。

import timefrom threading import Thread, RLock, current_threaddefshow_info1(lock):for _ in range(10):with lock:            print(f'\t {current_thread().name} AA', end='')            print('AA', end='')            print('AA')        time.sleep(0.01)defshow_info2(lock):for _ in range(10):with lock:            print(f'\t {current_thread().name} OO', end='')            print('OO', end='')            print('OO')if __name__ == '__main__':    lock = RLock()    t1 = Thread(target=show_info1, args=(lock,), name='show_info1')    t2 = Thread(target=show_info2, args=(lock,), name='show_info2')    t1.start()    t2.start()

示例二:不要让两个窗口卖出同一张票。

import timefrom threading import Thread, RLock, current_threadcurrent = 1defsale(lock):global currentwhileTrue:with lock:if current <= 20:                print(f'{current_thread().name}出售了第{current}张票!')                current += 1else:                print('票已售空')break        time.sleep(0.3)if __name__ == '__main__':    lock = RLock()    t1 = Thread(target=sale, name='窗口1', args=(lock,))    t2 = Thread(target=sale, name='窗口2', args=(lock,))    t3 = Thread(target=sale, name='窗口3', args=(lock,))    t1.start()    t2.start()    t3.start()

上面示例,如果不使用锁,运行结果则会出现两个窗口售出了同一张票。

最新文章

随机文章