『Python底层原理』--GIL对多线程的影响
在 Python 多线程编程中,全局解释器锁(Global Interpreter Lock,简称 GIL)是一个绕不开的话题。GIL是CPython解释器的一个机制,它限制了同一时刻只有一个线程可以执行 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-1和Thread-2。操作系统会为每个线程分配一个** OS 线程**,并在适当的时候切换它们的执行。
不过,Python中的多线程与其他语言不一样的地方在于,它有一个GIL的机制。
GIL是Python解释器的一个重要机制,一个线程在进入运行之前,必须先获得 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密集型任务的影响
GIL对 CPU 密集型任务的影响巨大,使得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带来了诸多限制,但移除它并非易事。主要困难包括:
[*]垃圾回收机制: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 中。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]