python快速入门学习笔记(进阶篇)三十七:GIL 全局解释器锁

  • 原创
  • 作者:程序员三丰
  • 发布时间:2026-06-05 12:30
  • 浏览量:8
Python入门第三十七课,主要是学习了解了全局解释器锁GIL,GIL是CPython的全局锁,确保同一时刻只有一个线程执行字节码,简化内存管理但限制了多线程的CPU并行能力。

一、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++扩展:

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

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

线程池优化:

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

异步编程(asyncio):

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

五、GIL 对比 Lock/RLock

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

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

import time
from threading import Thread, RLock, current_thread


def show_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)


def show_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 time
from threading import Thread, RLock, current_thread

current = 1


def sale(lock):
    global current
    while True:
        with lock:
            if current <= 20:
                print(f'{current_thread().name}出售了第{current}张票!')
                current += 1
            else:
                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()

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

声明:本文为原创文章,51blog.xyz和作者拥有版权,如需转载,请注明来源于51blog.xyz并保留原文链接:https://www.51blog.xyz/article/139.html

文章归档

推荐文章

buildadmin logo
Thinkphp8 Vue3 Element PLus TypeScript Vite Pinia

🔥BuildAdmin是一个永久免费开源,无需授权即可商业使用,且使用了流行技术栈快速创建商业级后台管理系统。

热门标签

PHP ThinkPHP ThinkPHP5.1 Go Mysql Mysql5.7 Redis Linux CentOS7 Git HTML CSS CSS3 Javascript JQuery Vue LayUI VMware Uniapp 微信小程序 docker wiki Confluence7 学习笔记 uView ES6 Ant Design Pro of Vue React ThinkPHP6.0 chrome 扩展 翻译工具 Nuxt SSR 服务端渲染 scrollreveal.js ThinkPHP8.0 Mac webman 跨域CORS vscode GitHub ECharts Canvas vue3 three.js 微信支付 PHP全栈开发 Python AI 人工智能 AI生成 工作经验 实战笔记