近日在学习异步编程了解并发时,留意到 Python 的多线程能力受限。

这让我想起我导师曾给我说过的一句话 —— Python 没有真正的多线程。这是因为GIL的存在。

GIL(Global Interpreter Lock,全局解释器锁) 是 Python 中的一种机制,用于保证同一时刻只有一个线程能够执行 Python 字节码。这一设计初衷是为了简化 CPython(Python 的主流实现)对内存管理的实现,特别是管理对象的引用计数,防止多线程间的资源竞争问题。

首先我们一定要明确一个观点,GIL 锁的是线程,而不是进程。

这里,就先再补充一下进程和线程的操作系统相关知识:

什么是线程和进程?

  • 进程(Process)
    • 独立的运行环境:每个进程有独立的内存空间和资源,不与其他进程共享。
    • 数据隔离:进程之间的数据互不影响,独立运行。
    • 真正的并行:在多核 CPU 上,不同的进程可以在不同核心上同时并行执行。
    • 开销较大:进程的创建和管理消耗较多的资源和内存。
  • 线程(Thread)
    • 共享内存空间:同一进程中的所有线程共享内存空间,包括全局变量、堆等。
    • 数据共享:线程之间可以直接访问共享数据,这带来了数据竞争问题,需要同步控制(如锁)。
    • 并发执行:在单核 CPU 上通过时间片轮转实现伪并行;在多核 CPU 上可以并行执行。
    • 开销较小:线程的创建和切换比进程轻量,适用于需要快速响应的小任务。

什么是 GIL 限制?

在 CPython 中,GIL 是一个全局的互斥锁,限制了多线程并发的能力。每个线程在执行 Python 代码时,必须先获取 GIL 锁,而 GIL 保证了同一时刻只有一个线程可以持有它。即使在多核 CPU 上,GIL 也会限制 Python 只能使用一个核心运行 Python 字节码,其他线程即使在 CPU 上待命,也无法同时执行字节码。这意味着在 CPU 密集型任务中,GIL 实际上会使得多线程执行效果变差,因为只有一个线程能执行,其他线程只能等待。

为什么说 GIL 限制了 Python 的多线程能力?

由于 GIL 的存在,Python 的多线程在处理 CPU 密集型任务时受到了严重限制。在单线程持有 GIL 时,其他线程无法执行 Python 代码,即使在多核 CPU 上也无法实现真正的多线程并行。这导致了以下问题:

  • 性能瓶颈:对于 CPU 密集型任务(如复杂计算、加密解密等),GIL 会成为性能瓶颈,线程之间的切换并不会带来性能提升,反而可能因频繁的 GIL 切换导致性能下降。

  • 线程并发低效:Python 的线程并发在 CPU 密集型任务中没有明显的优势,因为 GIL 限制了只有一个线程能执行字节码。多个线程抢占 GIL 的过程会引发频繁的锁切换,影响程序性能。

Python 没有真正的多线程?

Python 是支持多线程的,但 GIL 限制了 Python 在多线程下的并行能力。对于 I/O 密集型任务(如网络请求、文件读写等),Python 的多线程仍然可以发挥并发优势,因为等待 I/O 时线程可以释放 GIL,让其他线程执行。但是对于 CPU 密集型任务,Python 的多线程因 GIL 的限制无法充分利用多核 CPU。

那么,Python 有多进程吗?

实际上,Python 是支持真正的多进程的,且可以绕过 GIL 限制。在 Python 中,通过 multiprocessing 模块可以实现多进程,每个进程拥有独立的 GIL 和 Python 解释器,这样不同进程可以在多核 CPU 上并行执行。

然而,虽然多进程可以绕开 GIL 限制,实现并行计算,但 Python 的多进程并不总是最佳选择,主要原因如下:

  1. 内存开销高:每个进程都有独立的内存空间和 Python 解释器,创建和管理多个进程比多线程消耗更多的内存。

  2. 进程间通信复杂:多进程需要通过进程间通信(IPC)共享数据,常用的 IPC 包括管道、队列、共享内存等,这些方法会带来一定的性能开销和复杂性。

  3. 启动开销:创建进程的开销比线程更大,特别是在启动大量小任务时,多进程的启动和销毁时间较长,不如多线程高效。

如何应对 GIL 的限制?

Python 在某些场景下可以绕开 GIL 限制,实现真正的并行:

  1. 多进程:对于 CPU 密集型任务,可以使用 multiprocessing 模块创建多个进程,每个进程都有独立的 GIL,可以实现多核并行。

  2. 调用 C/C++ 扩展:可以通过编写 C/C++ 扩展代码,利用 Python 的 ctypescffi 等库调用 C/C++ 库。GIL 在调用非 Python 代码(如 C/C++)时可以释放,让多线程并行工作。

  3. 使用其他 Python 实现:如 JythonIronPython,它们没有 GIL 限制,但它们的兼容性和性能可能不如 CPython 高;还有 PyPy,它在特定场景下对多线程的支持更好。

  4. 协程和异步编程:对于 I/O 密集型任务,可以使用 asyncio 和协程来实现异步并发,避免 GIL 对多线程的限制,但协程并不是多线程或多进程,适用于 I/O 密集任务,而不是 CPU 密集型任务。