文章目录

  • 什么是GIL?
  • 为什么会有GIL?
  • GIL是Python的语言特性吗?
  • 没有GIL会发生什么?
  • 举例
  • 有GIL为什么还需要线程锁?
  • 附注


什么是GIL?

GIL(Global Interpreter Lock)全局解释器锁;
GIL (Global Interpreter Lock) 是 Python 部分解释器的一个重要特性。GIL 是一个全局锁,它限制了【一个进程】一次只能有【一个线程】在运行 Python 解释器中的字节码。即使你的程序有多个线程,在任意时刻,只有一个线程可以执行 Python 代码。

为什么会有GIL?

GIL 的设计是为了解决 Python 的多线程环境中的同步问题。在 Python 中,所有的数据都是全局的,如果多个线程同时访问和修改共享数据,可能导致不一致性和数据损坏。

GIL 的设计是为了避免这种情况,使得所有的线程在任意时刻只有一个线程在执行 Python 代码。这保证了在多线程环境中,所有数据的一致性。

但是,GIL 的设计也带来了一些副作用,尤其是在 CPU 密集型任务中。GIL 的存在限制了 CPU 利用率,导致 Python 的多线程性能较差。因此,在 CPU 密集型任务中,通常使用多进程代替多线程;

在IO密集型任务中,虽然有GIL的存在,单多线程还是能够提升一定的执行效率。
因为在IO密集型任务中,使用CPU的时间并不多,主要瓶颈还是在IO等待上;
单线程和多线程进行比较时,会发现多线程的执行速度比单线程要快很多,因为大多数时间都在等待IO操作,单线程必须等前一个IO操作完成后才能处理后面的程序,而使用多线程时,若当前线程进入了IO操作,解释器会很快将切换其他线程进行运行,一定程度上将多个线程的IO操作变成了类【并行】运行,非IO程序的执行多线程还是在【并发】执行。

GIL是Python的语言特性吗?

  • 不是
  1. 首先,Python是一门解释型的编程语言,在语言运行时会被逐行读取并解释执行;
    这与编译型语言形成对比,编译性语言需要提前进行整体编译,运行时将直接执行编译过后的文件或内容,但预期带来的不便就是在维护代码时会变得复杂、繁琐且不灵活;
  2. 其次,既然是解释型语言,就需要有解释器进行解释翻译;Python常用的解释器有:CPython、IPython、Pypy、Jpython等;
    通常大家使用的都是CPython,GIL是【CPython】与【CPython衍生解释器】(IPython等)的技术特性,针对于如Pypy、Jpython等解释器并不包含;

没有GIL会发生什么?

如果没有GIL,多线程程序将能够充分利用多核系统的计算能力,进一步提高程序的执行效率。但是,同时也会带来一些问题:

  1. 原子性问题:没有GIL保护,多个线程同时对同一个数据进行更新,很可能导致数据不一致的问题。
  2. 竞争条件问题:如果多个线程同时对共享资源进行操作,很可能会出现竞争条件,导致程序的不正确性。
  3. 同步问题:如果多个线程需要协调合作完成某项任务,就需要进行同步操作,保证多个线程的执行顺序。

如果没有GIL,就需要程序员自己解决这些问题,有些人认为GIL是CPython的一个缺陷,但其实GIL可以看作是一种取舍,既能保证解释器在多线程下的部分线程安全,又能减少程序员的工作量。

举例

  • 举例一
    假设有一个 Python 程序,它计算某个数的阶乘。该程序运行了一个很长的循环,并且在循环期间需要执行许多数学计算。
    如果不使用 GIL,在多核 CPU 上运行该程序时,可能会有多个线程同时执行计算。这可能会导致多个线程试图修改同一内存区域,从而导致错误或数据丢失。
    但是,由于 GIL 的存在,仅允许一个线程同时执行。这意味着,在多核 CPU 上运行该程序时,一次只有一个线程可以访问内存。因此,不会出现同时修改内存的问题,从而避免了数据竞争问题。
  • 举例二
    假设有一个 Python 程序,该程序在运行大量图像处理任务,每个任务都需要大量的 CPU 时间。如果该程序在单核 CPU 上运行,它将非常慢,因此我们希望将其运行在多核 CPU 上。
    如果不使用 GIL,在多核 CPU 上运行该程序时,多个线程可能同时执行处理任务。这可能导致内存数据错误,因为多个线程可能会同时试图修改同一数据。
    但是,由于 GIL 的存在,仅允许一个线程同时执行处理任务。这意味着,在多核 CPU 上运行该程序时,仅有一个线程可以同时处理图像,其他线程必须等待当前处理任务完成。

因此,GIL 的作用是保证在多线程 Python 程序中的内存安全,它确保仅有一个线程同时访问内存,从而避免了多线程并发访问导致的数据错误。

注意:GIL只保证解释器下的Python程序在多线程下不会出现系统内存错误,并【不】保证数据的【正确性】;

有GIL为什么还需要线程锁?

上面提到了GIL并不保证数据的正确性。

如果有一个文本,有两个线程,A线程向文本中写入1234,B线程向文本中写入ABCD,我们先启动线程A再启动线程B,期望得到1234ABCD或者ABCD1234的结果;

但GIL只能保证线程A、B向文件写入时不会出现内存错误(不会出现一个字符是1的上半部分和A的下半部分[不是一个有效的字符]),不能保证最后写入顺序变成1A2BC3D4或者1A2B3C4D等。

想要得到正确的结果就需要保证先执行完A线程,再执行B线程 或者 先执行完B线程,再执行完A线程。

就得给A线程中写入文件处加线程锁,就算线程被切换到B,应为程序被加锁,B无法获取到锁只能等待调度到A继续执行,直到A释放锁后,B才能进行文件写入;

上面只是举了一个不恰当的例子方便理解,通常在多线程进行大量数据运算或者大流量文件访问时会遇到现象;

附注

  1. Python解释器
    在运行 Python 程序时,会读取源代码并执行它。下面是 Python 解释器运行的简要流程:
  1. 读取源代码:首先,解释器读取源代码,并将其转换为字节码。
  2. 编译字节码:然后,解释器将字节码编译为机器可以识别的代码。
  3. 执行字节码:最后,解释器执行字节码,并产生结果。

这个流程可以在内存中进行,因此 Python 程序不需要预先编译。这使得 Python 程序更加灵活和易于维护。
同时,因为 Python 解释器在运行时对代码进行编译,因此可以对代码进行动态优化,从而提高代码的执行效率。

  1. CPU 密集型任务与IO 密集型任务
    CPU 密集型任务是指大量使用 CPU 计算的任务,如数学计算、图形处理等。这类任务需要大量使用 CPU 进行计算,因此需要快速的 CPU 处理速度来保证程序的效率。
    IO 密集型任务是指大量需要等待 IO 操作的任务,如读写文件、网络通信等。这类任务需要大量的 IO 操作,因此需要优化 IO 操作的效率来保证程序的效率。
  2. GIL与多进程
    每个独立的Python进程都有单独的GIL。
    GIL是Python解释器的一个全局锁,保证同一时刻只有一个线程可以执行Python代码。但是,每个独立的Python进程都有自己的内存空间,因此各个进程中的GIL互不影响。
    因此,如果使用多个独立的Python进程,每个进程中都有自己的GIL,这些GIL互不影响,可以利用CPU的多核特性同时运行多个进程的多个线程(每个进程每时每刻只有一个线程在运行),从而充分利用多核处理器的优势。
  3. 多线程与多进程
    Python官方并不明确地表示不推荐使用多线程,但是由于GIL的存在,使用多线程在Python中通常并不是最佳选择。
    GIL限制了同一时刻只有一个线程可以执行Python代码,因此即使使用多线程,也无法充分利用多核处理器的优势。而多进程不受GIL的影响(因为每个进程每时每刻都有一个线程在跑,有几个进程,就相当于同时有多少个线程在跑),因此可以利用多核处理器的计算能力。
    当然,多进程也有其自身的问题,例如进程间通信比线程间通信更复杂,并且内存消耗更多。但是,如果代码需要大量的 CPU 计算,多进程是更优秀的选择。
    在 Python 中选择使用多线程还是多进程取决于具体需求:
  1. 对于多核 CPU 和高性能计算,多进程通常是更好的选择。
  2. 对于I/O密集型任务,多线程可以适当提高执行