English 简体中文 繁體中文 한국 사람 日本語 Deutsch русский بالعربية TÜRKÇE português คนไทย french
查看: 1|回复: 0

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

[复制链接]
查看: 1|回复: 0

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

[复制链接]
查看: 1|回复: 0

223

主题

0

回帖

679

积分

高级会员

积分
679
SRPE6ob4Q8m

223

主题

0

回帖

679

积分

高级会员

积分
679
5 天前 | 显示全部楼层 |阅读模式
在 Python 多线程编程中,全局解释器锁(Global Interpreter Lock,简称 GIL)是一个绕不开的话题。
GIL是CPython解释器的一个机制,它限制了同一时刻只有一个线程可以执行 Python 字节码。
尽管多线程在某些场景下可以显著提升程序性能,但 GIL 的存在却让 Python 多线程在很多情况下无法充分发挥其优势。
本文将探讨 GIL 的工作机制、它对 Python 多线程的影响,以及解决相关问题的方法和未来的发展方向。
1. Python的多线程

当我们运行一个 Python 可执行文件时,操作系统会启动一个主线程。
这个主线程负责执行 Python 程序的初始化操作,包括加载模块、编译代码以及执行字节码等。
在多线程环境中,Python 线程由操作系统线程(OS 线程)和 Python 线程状态组成,
操作系统线程负责调度线程的执行,而 Python 线程状态则包含了线程的局部变量、堆栈信息等。
比如:
import threadingdef 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-1和Thread-2。操作系统会为每个线程分配一个** OS 线程**,并在适当的时候切换它们的执行。
不过,Python中的多线程与其他语言不一样的地方在于,它有一个GIL的机制。
GIL是Python解释器的一个重要机制,一个线程在进入运行之前,必须先获得 GIL。
如果 GIL 已被其他线程占用,那么当前线程将等待,直到 GIL 被释放。
GIL 的释放规则如下:

  • 线程执行一定时间后,会主动释放 GIL,以便其他线程可以获取它
  • 线程在执行 I/O 操作时,会释放 GIL,因为 I/O 操作通常会阻塞线程,释放 GIL 可以让其他线程有机会运行。
比如:
import timedef cpu_bound_task():    # 模拟 CPU 密集型任务    result = 0    for i in range(10000000):        result += idef 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密集型任务的影响

GIL对 CPU 密集型任务的影响巨大,使得Python的多线程在CPU密集型任务中几乎无法发挥优势。
因为即使有多个线程,同一时刻也只有一个线程可以执行 Python 字节码。
而且,线程之间的上下文切换还会增加额外的开销,导致程序性能下降。
import timeimport threadingdef cpu_bound_task():    result = 0    for i in range(10000000):        result += idef 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 timeimport threadingdef 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 timeimport threadingdef cpu_bound_task():    result = 0    for i in range(10000000):        result += idef 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带来了诸多限制,但移除它并非易事。主要困难包括:

  • 垃圾回收机制:Python 的垃圾回收机制依赖于引用计数,移除 GIL 后需要重新设计垃圾回收机制
  • C 扩展兼容性:许多现有的 C 扩展模块依赖于 GIL 来保证线程安全。移除 GIL 后,这些扩展模块可能需要重新编写
例如,Gilectomy项目尝试移除 GIL,但最终因性能问题和兼容性问题而失败。
虽然移除了 GIL,但单线程性能大幅下降,且许多 C 扩展模块无法正常工作。
GIL的实现细节可以通过阅读CPython源代码来进一步了解。
关键文件包括Python/ceval.c和Python/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 Gross 的 CPython fork 是一个值得关注的尝试,他成功移除了 GIL,并且在单线程性能上取得了显著提升。
他的工作为解决 GIL 问题带来了新的方向,但目前尚未被合并到主线 CPython 中。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

223

主题

0

回帖

679

积分

高级会员

积分
679

QQ|智能设备 | 粤ICP备2024353841号-1

GMT+8, 2025-3-11 03:27 , Processed in 1.324554 second(s), 30 queries .

Powered by 智能设备

©2025

|网站地图