面试官:你来介绍一下Python全局解释锁

部署运行你感兴趣的模型镜像

本篇文章介绍了 Python 全局解释锁的作用、释放 GIL 的时机、如何绕过 GIL 等,相信能够助力你通过面试官的拷问。

Python 的全局解释锁(Global Interpreter Lock, GIL)是 CPython 解释器(Python 官方实现)中的一个核心机制,用于确保同一时刻只有一个线程能够执行 Python 字节码。

1. GIL 是如何工作的?

GIL 本质上是一个互斥锁(mutex),确保在任何时刻只有一个线程在执行 Python 代码。当一个线程获取 GIL 后,其他线程必须等待该线程释放GIL后才能执行。即使在多核处理器上,同一时刻也只能有一个线程执行 Python 代码。

下面这张图,就是一个 GIL 在 Python 程序的工作示例。其中,Thread 1、2、3 轮流执行,每一个线程在开始执行时,都会锁住 GIL,以阻止别的线程执行;同样的,每一个线程执行完一段后,会释放 GIL,以允许别的线程开始利用资源。

2. GIL 的作用

GIL 的作用主要有三个:

  1. 简化内存管理。确保引用计数操作的原子性,防止内存泄漏和数据竞争。
    Python 的内存管理。Python 主要通过引用计数(reference counting)进行内存管理。
    • 引用计数操作:每个 Python 对象都有一个引用计数器,记录有多少个变量引用这个对象。
      • 当创建对象或者新的变量引用该对象时,引用计数+1;
      • 当引用被删除或离开作用域时,引用计数-1;
      • 当引用计数降为 0 时,对象被销毁,内存被释放。
    • 引用计数的实现: 在CPython中,每个 PyObject 结构体都包含一个整数字段ob_refcnt,用于记录引用计数。
  2. 简化 CPython 解释器的实现。 GIL大大简化了 CPython 解释器的实现。不需要复杂的锁机制来保护共享数据结构。
  3. 保证C扩展的安全。让非线程安全的 C 库可以安全地在Python中使用。

3. 线程什么时候会释放 GIL?

  1. I/O操作:在执行I/O操作时,Python会释放GIL允许其他线程执行。
  2. 长时间计算:Python解释器会定期释放GIL(默认每100个字节码指令),给其他线程执行的机会。
  3. 内存安全保证
    • 当释放GIL时,确保当前线程不会执行任何可能修改共享Python对象的操作
    • C 扩展在执行可能修改Python对象的操作前必须重新获取GIL
    • 这确保了即使GIL被临时释放,内存管理操作仍然是安全的

4. 如何绕过 GIL?

  1. 使用多进程。通过multiprocessing库可以启动多个Python进程,每个进程有自己的GIL。
  2. 使用非CPython解释器。如 Jython 和 IronPython 没有GIL。
  3. 使用 C 扩展。编写 C 扩展并在其中释放GIL。
  4. 使用并行计算库。如NumPy、Pandas等库的许多操作在C语言层面实现,会在计算期间释放GIL。

当然,如果你的程序需要高性能,那么选择 Python 语言可能是一个错误的决定。

5. 有了全局解释锁,为什么还需要线程间同步机制?

尽管Python有全局解释锁(GIL),但线程同步机制(如互斥锁、信号量、条件变量等)仍然是必要的,原因如下:

  1. GIL的保护范围有限。GIL只保证Python字节码的执行是原子的,但无法保证更高级别操作的原子性:

    • 字节码级别的原子性:GIL确保同一时刻只有一个线程执行Python字节码,但一个逻辑操作通常由多个字节码指令组成。
    • 操作的非原子性:例如 counter += 1 实际包含至少三个步骤:读取counter值、增加值、写回counter。在这些步骤之间,GIL可能被释放并切换到其他线程。
  2. GIL的释放与切换。 Python解释器会在以下情况释放GIL:

    • 定期释放:解释器每执行一定数量的字节码指令(默认100个),会自动释放GIL并允许其他线程运行。
    • I/O操作时释放:执行I/O操作时,解释器会释放GIL,让其他线程有机会执行。
    • 阻塞操作时释放:在某些阻塞操作(如等待网络响应)期间,GIL也会被释放。

下面是一个 GIL无法保证高级操作原子性的例子:

import threading
import time

counter = 0

def increment_counter(n):
    global counter
    for _ in range(n):
        # 强制增加线程切换的可能性
        current = counter
        # 人为增加线程被切换的几率
        time.sleep(0.000001)  # 微小的延迟
        counter = current + 1

# 使用更大的循环次数
iterations = 1000
t1 = threading.Thread(target=increment_counter, args=(iterations,))
t2 = threading.Thread(target=increment_counter, args=(iterations,))

t1.start()
t2.start()
t1.join()
t2.join()

print(f"预期结果: {iterations * 2}")
print(f"实际结果: {counter}")

6. 小结

GIL 的主要作用是简化 Python 的内存管理和 CPython 解释器的实现,以及确保 C 扩展的安全。

尽管 GIL 确保了同一时刻只有一个线程执行Python字节码,但它不能保证高级操作的原子性。Python的线程在执行过程中仍可能在字节码指令之间切换,导致数据不一致。因此,当多个线程访问共享资源时,适当的同步机制(如锁、信号量、条件变量等)仍然是必要的,以确保数据一致性和程序的正确行为。

今天的文章就到这里了。觉得有帮助的道友,不要忘记一键三连呐。

您可能感兴趣的与本文相关的镜像

Python3.11

Python3.11

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值