『Python底层原理』--GIL对多线程的影响

Python 多线程编程中,全局解释器锁(Global Interpreter Lock,简称 GIL)是一个绕不开的话题。

GILCPython解释器的一个机制,它限制了同一时刻只有一个线程可以执行 Python 字节码。

尽管多线程在某些场景下可以显著提升程序性能,但 GIL 的存在却让 Python 多线程在很多情况下无法充分发挥其优势。

本文将探讨 GIL 的工作机制、它对 Python 多线程的影响,以及解决相关问题的方法和未来的发展方向。

1. Python的多线程

当我们运行一个 Python 可执行文件时,操作系统会启动一个主线程。

这个主线程负责执行 Python 程序的初始化操作,包括加载模块、编译代码以及执行字节码等。

在多线程环境中,Python 线程由操作系统线程(OS 线程)和 Python 线程状态组成,

操作系统线程负责调度线程的执行,而 Python 线程状态则包含了线程的局部变量、堆栈信息等。

比如:

import threading

def worker():
    print(f"Thread {threading.current_thread().name} is running")

# 创建并启动两个线程
thread1 = threading.Thread(target=worker, name="Thread-1")
thread2 = threading.Thread(target=worker, name="Thread-2")
thread1.start()
thread2.start()
thread1.join()
thread2.join()

在上述代码中,我们创建了两个线程Thread-1Thread-2。操作系统会为每个线程分配一个** OS 线程**,并在适当的时候切换它们的执行。

不过,Python中的多线程与其他语言不一样的地方在于,它有一个GIL的机制。

GILPython解释器的一个重要机制,一个线程在进入运行之前,必须先获得 GIL

如果 GIL 已被其他线程占用,那么当前线程将等待,直到 GIL 被释放。

GIL 的释放规则如下:

  • 线程执行一定时间后,会主动释放 GIL,以便其他线程可以获取它
  • 线程在执行 I/O 操作时,会释放 GIL,因为 I/O 操作通常会阻塞线程,释放 GIL 可以让其他线程有机会运行。

比如:

import time

def cpu_bound_task():
    # 模拟 CPU 密集型任务
    result = 0
    for i in range(10000000):
        result += i

def io_bound_task():
    # 模拟 I/O 密集型任务
    time.sleep(2)

# 创建两个线程分别执行 CPU 密集型和 I/O 密集型任务
thread_cpu = threading.Thread(target=cpu_bound_task)
thread_io = threading.Thread(target=io_bound_task)
thread_cpu.start()
thread_io.start()
thread_cpu.join()
thread_io.join()

在上述代码中,cpu_bound_task是一个 CPU 密集型任务,它会一直占用 GIL,直到任务完成。

io_bound_task是一个 I/O 密集型任务,它在执行时会释放 GIL,让其他线程有机会运行。

2. GIL的影响

2.1. 对CPU密集型任务的影响

GILCPU 密集型任务的影响巨大,使得Python的多线程在CPU密集型任务中几乎无法发挥优势。

因为即使有多个线程,同一时刻也只有一个线程可以执行 Python 字节码。

而且,线程之间的上下文切换还会增加额外的开销,导致程序性能下降。

import time
import threading

def cpu_bound_task():
    result = 0
    for i in range(10000000):
        result += i

def single_thread():
    start_time = time.time()
    cpu_bound_task()
    cpu_bound_task()
    print(f"Single-thread time: {time.time() - start_time:.2f} seconds")

def multi_thread():
    start_time = time.time()
    thread1 = threading.Thread(target=cpu_bound_task)
    thread2 = threading.Thread(target=cpu_bound_task)
    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()
    print(f"Multi-thread time: {time.time() - start_time:.2f} seconds")

single_thread()
multi_thread()

运行上述代码,我们会发现多线程版本的执行时间比单线程版本还要长,这正是因为 GIL 的存在导致了线程之间的上下文切换开销。

2.2. 对I/O密集型任务的影响

CPU 密集型任务不同,多线程在 I/O密集型任务中可以显著提升性能。

因为当一个线程在执行 I/O 操作时,它会释放 GIL,其他线程可以利用这段时间执行其他任务。

import time
import threading

def io_bound_task():
    time.sleep(2)

def single_thread():
    start_time = time.time()
    io_bound_task()
    io_bound_task()
    print(f"Single-thread time: {time.time() - start_time:.2f} seconds")

def multi_thread():
    start_time = time.time()
    thread1 = threading.Thread(target=io_bound_task)
    thread2 = threading.Thread(target=io_bound_task)
    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()
    print(f"Multi-thread time: {time.time() - start_time:.2f} seconds")

single_thread()
multi_thread()

运行上述代码,我们会发现多线程版本的执行时间比单线程版本缩短了一半,这说明多线程在 I/O 密集型任务中可以有效提升性能。

2.3. 护航效应(Convoy Effect)

CPU 密集型线程和 I/O 密集型线程混合运行时,会出现一种称为“护航效应”的现象。

CPU 密集型线程会一直占用 GIL,导致 I/O 密集型线程无法及时获取 GIL,从而大幅降低 I/O 密集型线程的性能。

比如:

import time
import threading

def cpu_bound_task():
    result = 0
    for i in range(10000000):
        result += i

def io_bound_task():
    time.sleep(2)

def mixed_thread():
    start_time = time.time()
    thread_cpu = threading.Thread(target=cpu_bound_task)
    thread_io = threading.Thread(target=io_bound_task)
    thread_cpu.start()
    thread_io.start()
    thread_cpu.join()
    thread_io.join()
    print(f"Mixed-thread time: {time.time() - start_time:.2f} seconds")

mixed_thread()

在上述代码中,cpu_bound_task会一直占用GIL,导致io_bound_task 无法及时运行,从而延长了整个程序的执行时间。

3. GIL存在的原因

GIL给并发性能带来了很多的问题,为什么Python解释器中会有GIL这个方案呢?

因为Python历史悠久,当初Python流行的时候,针对多核的并发编程并不是主流,当时采用GIL主要是为了保证线程安全。

GIL涵盖了以下几个方面:

  • 引用计数Python 使用引用计数来管理内存。如果多个线程同时修改引用计数,可能会导致内存泄漏或崩溃
  • 数据结构:许多 Python 内置数据结构(如列表、字典等)需要线程安全的访问
  • 全局数据:解释器的全局状态需要保护,以防止多线程访问时出现数据竞争
  • C 扩展:许多 C 扩展模块依赖于GIL来保证线程安全。

目前,尽管GIL带来了诸多限制,但移除它并非易事。主要困难包括:

  1. 垃圾回收机制Python 的垃圾回收机制依赖于引用计数,移除 GIL 后需要重新设计垃圾回收机制
  2. C 扩展兼容性:许多现有的 C 扩展模块依赖于 GIL 来保证线程安全。移除 GIL 后,这些扩展模块可能需要重新编写

例如,Gilectomy项目尝试移除 GIL,但最终因性能问题和兼容性问题而失败。

虽然移除了 GIL,但单线程性能大幅下降,且许多 C 扩展模块无法正常工作。

GIL的实现细节可以通过阅读CPython源代码来进一步了解。

关键文件包括Python/ceval.cPython/thread.c,其中定义了GIL的获取和释放机制。

4. GIL的未来

GIL是一定要解决的问题,毕竟多核才是当前主流的发展方向。

目前,有些项目为了解决GIL对并发性能的影响,正在努力发展中,包括:

4.1. 子解释器计划

Python 的子解释器计划(PEP 554)试图通过引入多个独立的解释器(每个解释器拥有自己的 GIL)来实现多解释器并行。

这种方法可以在一定程度上绕过 GIL 的限制,但目前仍存在一些限制,例如跨解释器通信的开销较大。

4.2. Faster CPython 项目

Faster CPython 项目专注于提升 Python 的单线程性能。

虽然它可能会进一步优化 GIL 的实现,但其主要目标是减少解释器的开销,而不是直接解决 GIL 问题。

这可能会使 GIL 问题在短期内受到较少的关注。

4.3. Sam Gross 的 CPython fork

Sam GrossCPython fork 是一个值得关注的尝试,他成功移除了 GIL,并且在单线程性能上取得了显著提升。

他的工作为解决 GIL 问题带来了新的方向,但目前尚未被合并到主线 CPython 中。

原创作者: wang_yb 转载于: https://www.cnblogs.com/wang_yb/p/18755276
Python中的全局解释器锁(GIL)是为了保护Python字节码的解释器核心同时执行,防止多个线程并发修改数据结构而引入的。这意味着,在任何时候,只有一个线程能够执行Python字节码,即使是在多核处理器上也是如此。因此,尽管Python支持多线程,但它并不适用于那些依赖大量CPU计算的任务(即CPU密集型),因为在这种情况下,线程切换频繁,CPU的实际利用率并没有提高。 如果你想要在Python中实现多线程,并且期望充分利用多核CPU,你需要关注以下几个方面: 1. **I/O密集型任务**:Python的`threading`库可以处理I/O密集型任务,因为在等待I/O响应时,GIL会被释放,其他线程可以执行。 2. **多进程**:Python的`multiprocessing`模块提供了一个无GIL的环境,因为它会启动新的进程,而不是共享相同的解释器实例,所以理论上可以实现真正的并行计算。 3. **使用多线程池**:如`concurrent.futures.ThreadPoolExecutor`,它可以帮助管理线程池,将任务分发给各个线程,让它们在等待I/O操作时保持忙碌。 4. **利用第三方库**:有些第三方库,如Celery或multiprocessing.dummy,虽然底层还是基于线程,但由于其工作原理,可能能在某些场景下减轻GIL影响。 需要注意的是,虽然以上措施可以在某种程度上提升性能,但要完全绕开GIL所带来的限制,可能需要采用更底层的技术,例如Cython扩展或使用Numba进行加速。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值