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

打印 上一主题 下一主题

主题 1581|帖子 1581|积分 4743

在 Python 多线程编程中,全局表明器锁(Global Interpreter Lock,简称 GIL)是一个绕不开的话题。
GIL是CPython表明器的一个机制,它限制了同一时刻只有一个线程可以执行 Python 字节码。
尽管多线程在某些场景下可以显着提升步伐性能,但 GIL 的存在却让 Python 多线程在许多环境下无法充分发挥其上风。
本文将探讨 GIL 的工作机制、它对 Python 多线程的影响,以及解决相关问题的方法和未来的发展方向。
1. Python的多线程

当我们运行一个 Python 可执行文件时,操作系统会启动一个主线程。
这个主线程负责执行 Python 步伐的初始化操作,包括加载模块、编译代码以及执行字节码等。
在多线程环境中,Python 线程由操作系统线程(OS 线程)和 Python 线程状态组成,
操作系统线程负责调度线程的执行,而 Python 线程状态则包罗了线程的局部变量、堆栈信息等。
比如:
  1. import threading
  2. def worker():
  3.     print(f"Thread {threading.current_thread().name} is running")
  4. # 创建并启动两个线程
  5. thread1 = threading.Thread(target=worker, name="Thread-1")
  6. thread2 = threading.Thread(target=worker, name="Thread-2")
  7. thread1.start()
  8. thread2.start()
  9. thread1.join()
  10. thread2.join()
复制代码
在上述代码中,我们创建了两个线程Thread-1和Thread-2。操作系统会为每个线程分配一个** OS 线程**,并在适当的时候切换它们的执行。
不过,Python中的多线程与其他语言不一样的地方在于,它有一个GIL的机制。
GIL是Python表明器的一个重要机制,一个线程在进入运行之前,必须先得到 GIL。
如果 GIL 已被其他线程占用,那么当前线程将等待,直到 GIL 被释放。
GIL 的释放规则如下:

  • 线程执行肯定时间后,会主动释放 GIL,以便其他线程可以获取它
  • 线程在执行 I/O 操作时,会释放 GIL,由于 I/O 操作通常会阻塞线程,释放 GIL 可以让其他线程有机会运行。
比如:
  1. import time
  2. def cpu_bound_task():
  3.     # 模拟 CPU 密集型任务
  4.     result = 0
  5.     for i in range(10000000):
  6.         result += i
  7. def io_bound_task():
  8.     # 模拟 I/O 密集型任务
  9.     time.sleep(2)
  10. # 创建两个线程分别执行 CPU 密集型和 I/O 密集型任务
  11. thread_cpu = threading.Thread(target=cpu_bound_task)
  12. thread_io = threading.Thread(target=io_bound_task)
  13. thread_cpu.start()
  14. thread_io.start()
  15. thread_cpu.join()
  16. thread_io.join()
复制代码
在上述代码中,cpu_bound_task是一个 CPU 密集型使命,它会一直占用 GIL,直到使命完成。
而io_bound_task是一个 I/O 密集型使命,它在执行时会释放 GIL,让其他线程有机会运行。
2. GIL的影响

2.1. 对CPU密集型使命的影响

GIL对 CPU 密集型使命的影响巨大,使得Python的多线程在CPU密集型使命中几乎无法发挥上风。
由于纵然有多个线程,同一时刻也只有一个线程可以执行 Python 字节码。
而且,线程之间的上下文切换还会增加额外的开销,导致步伐性能下降。
  1. import time
  2. import threading
  3. def cpu_bound_task():
  4.     result = 0
  5.     for i in range(10000000):
  6.         result += i
  7. def single_thread():
  8.     start_time = time.time()
  9.     cpu_bound_task()
  10.     cpu_bound_task()
  11.     print(f"Single-thread time: {time.time() - start_time:.2f} seconds")
  12. def multi_thread():
  13.     start_time = time.time()
  14.     thread1 = threading.Thread(target=cpu_bound_task)
  15.     thread2 = threading.Thread(target=cpu_bound_task)
  16.     thread1.start()
  17.     thread2.start()
  18.     thread1.join()
  19.     thread2.join()
  20.     print(f"Multi-thread time: {time.time() - start_time:.2f} seconds")
  21. single_thread()
  22. multi_thread()
复制代码

运行上述代码,我们会发现多线程版本的执行时间比单线程版本还要长,这正是由于 GIL 的存在导致了线程之间的上下文切换开销。
2.2. 对I/O密集型使命的影响

与 CPU 密集型使命不同,多线程在 I/O密集型使命中可以显着提升性能。
由于当一个线程在执行 I/O 操作时,它会释放 GIL,其他线程可以利用这段时间执行其他使命。
  1. import time
  2. import threading
  3. def io_bound_task():
  4.     time.sleep(2)
  5. def single_thread():
  6.     start_time = time.time()
  7.     io_bound_task()
  8.     io_bound_task()
  9.     print(f"Single-thread time: {time.time() - start_time:.2f} seconds")
  10. def multi_thread():
  11.     start_time = time.time()
  12.     thread1 = threading.Thread(target=io_bound_task)
  13.     thread2 = threading.Thread(target=io_bound_task)
  14.     thread1.start()
  15.     thread2.start()
  16.     thread1.join()
  17.     thread2.join()
  18.     print(f"Multi-thread time: {time.time() - start_time:.2f} seconds")
  19. single_thread()
  20. multi_thread()
复制代码

运行上述代码,我们会发现多线程版本的执行时间比单线程版本缩短了一半,这说明多线程在 I/O 密集型使命中可以有用提升性能。
2.3. 护航效应(Convoy Effect)

当 CPU 密集型线程和 I/O 密集型线程混淆运行时,会出现一种称为“护航效应”的征象。
CPU 密集型线程会一直占用 GIL,导致 I/O 密集型线程无法实时获取 GIL,从而大幅降低 I/O 密集型线程的性能。
比如:
  1. import time
  2. import threading
  3. def cpu_bound_task():
  4.     result = 0
  5.     for i in range(10000000):
  6.         result += i
  7. def io_bound_task():
  8.     time.sleep(2)
  9. def mixed_thread():
  10.     start_time = time.time()
  11.     thread_cpu = threading.Thread(target=cpu_bound_task)
  12.     thread_io = threading.Thread(target=io_bound_task)
  13.     thread_cpu.start()
  14.     thread_io.start()
  15.     thread_cpu.join()
  16.     thread_io.join()
  17.     print(f"Mixed-thread time: {time.time() - start_time:.2f} seconds")
  18. 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 中。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

张春

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表