一.Python 中的 GIL
Python 中的全局解释器锁(Global Interpreter Lock,GIL)是 CPython 解释器的一个机制,用来确保在多线程环境下,只有一个线程可以执行 Python 字节码,任何时刻只能有一个线程在执行 Python 代码。这意味着,即使有多个 CPU 核心,Python 的多线程程序在同一时间也只有一个线程在执行,无法充分利用多核处理器的优势。
多核处理器是指在同一个物理处理器芯片上集成了多个独立的处理核心(也称为"核")。每个核心都可以独立执行指令,具备自己的运算单元、寄存器等,从而允许同时处理多个任务或线程。多核处理器的出现是为了应对单核处理器频率提高所带来的功耗和散热问题,同时显著提升计算性能。简单理解,多核处理器中的核指的就是逻辑 CPU。
1.GIL 存在原因
GIL 设计初衷是为简化 CPython 解释器中对内存管理处理。在 Python 中内存管理使用引用计数机制(reference counting)。GIL 确保了内存管理的线程安全性,避免了多线程环境下的内存管理问题,如内存泄漏或数据竞争。
2.GIL 的影响
(1)多线程并发受限
由于 GIL 的存在,即使开启了多线程,Python 程序在 CPU 密集型任务(如大量计算)中也不能真正实现多线程并发。所有线程必须轮流获取 GIL,这导致多线程程序的性能提升有限甚至可能变差。
(2)I/O 密集型任务
对于 I/O 密集型任务(如文件读写、网络请求),GIL 的影响较小。因为 I/O 操作通常会等待系统响应,Python 可以在一个线程等待 I/O 的同时释放 GIL,允许其他线程运行,因此在 I/O 密集型任务中多线程仍然有优势。
3.如何绕过 GIL
虽然 GIL 在 CPython 中存在限制,但有几种方法可以绕过或减少其影响。比如,多进程(multiprocessing)、使用其它解释器(Jython|IronPython)、C 扩展模块、多线程 +I/O 密集型任务。
二.GIL 与多进程
GIL(全局解释器锁)与多进程是两个不同的概念,但它们在多核计算中的应用息息相关,尤其在 Python 中。
1.GIL 与多进程关系
在 Python 中,GIL 限制了多线程的性能,但多进程可以通过为每个进程分配独立的 GIL 来解决这个问题。通过使用多进程模型,程序可以充分利用多核处理器,实现真正的并行计算。
2.多进程的实现
Python 提供了 multiprocessing
模块,使得在 Python 中使用多进程编程变得相对简单。通过 multiprocessing
,可创建多个进程,每个进程独立运行,不共享内存,能够绕过 GIL 并实现并行计算。
python
from multiprocessing import Process
def cpu_bound_task(n):
result = 0
for i in range(n):
result += i * i
print(f"Result: {result}")
if __name__ == "__main__":
processes = []
for _ in range(4): # 创建四个进程
p = Process(target=cpu_bound_task, args=(10000000,))
processes.append(p)
p.start()
for p in processes:
p.join() # 等待所有进程完成
在这个例子中,cpu_bound_task
是一个 CPU 密集型任务,使用了 multiprocessing.Process
来创建多个进程,这些进程可以在不同的 CPU 核心上并行运行,避免了 GIL 的限制。
3.多进程的优缺点
(1)优点
- 绕过 GIL 限制:每个进程都有独立的 GIL,能够在多核 CPU 上并行执行多个进程,显著提升 CPU 密集型任务的性能。
- 进程隔离:每个进程拥有独立的内存空间,进程之间不会相互影响,这提高了安全性。
(2)缺点
- 进程开销:进程比线程更重,每个进程都有独立的内存空间,因此进程的启动和内存占用成本更高。
- 进程间通信复杂:由于进程之间不共享内存,必须使用进程间通信(如管道、队列、共享内存)来交换数据,这比线程间的通信更复杂。
- 内存消耗大:因为每个进程都有独立的内存空间,处理大量数据时可能会占用更多的内存。
4.使用场景
(1)多线程适合 I/O 密集型任务
由于 I/O 操作(如网络请求、文件操作)会释放 GIL,因此在这些场景中使用多线程能够有效提高效率。
(2)多进程适合 CPU 密集型任务
对于需要大量计算的任务,如图像处理、数据分析和科学计算,使用多进程可以绕过 GIL,充分利用多核 CPU,提升性能。
三.GIL 与多线程
GIL(全局解释器锁)和多线程在 Python 中的关系是编写并发程序时需要特别注意的关键点。尽管 Python 支持多线程编程,但由于 GIL 的存在,多线程在 Python 中的性能在某些情况下受到了限制。
1.GIL 与多线程的限制
在 GIL 的约束下,Python 的多线程实现的效率在以下场景中受到限制:
(1)CPU 密集型任务
进行大量数学计算或图像处理等需要 CPU 大量计算的任务。由于 GIL 的存在,Python 的多个线程在这种情况下并不能真正并行执行,而是轮流获取 GIL 执行任务,因此不能充分利用多核处理器的优势。
(2)频繁切换 GIL
多线程程序中,线程频繁获取和释放 GIL 会导致线程间的上下文切换,这带来了额外的开销,反而可能降低整体程序的性能。
2.GIL 对 I/O 密集型任务影响
虽然 GIL 对 CPU 密集型任务限制很大,但对 I/O 密集型任务的影响较小。在进行 I/O 操作(如文件读写、网络请求)时,线程会等待外部资源的响应,而此时线程会释放 GIL,使得其它线程可以运行。因此,I/O 密集型任务如网络爬虫、数据库操作等,多线程仍然能显著提高效率。
3.GIL 下有效使用多线程
(1)I/O 密集型任务
对于需要频繁进行 I/O 操作的任务,如网络请求、文件操作等,多线程能够有效地提升程序的并发性。在这些任务中,线程在等待 I/O 时会释放 GIL,其它线程可以执行。
python
import threading
import time
def io_task():
print("Starting I/O task")
time.sleep(2) # 模拟I/O操作
print("I/O task finished")
threads = []
for i in range(4):
t = threading.Thread(target=io_task)
threads.append(t)
t.start()
for t in threads:
t.join()
(2)使用外部库
对于 CPU 密集型任务,可以借助使用 C/C++ 实现的外部库,如 NumPy、SciPy 等。因为这些库的大量计算部分是用 C 语言编写的,它们可以在不受 GIL 限制的情况下并行执行。GIL 只在执行 Python 字节码时才有效,但在执行 C 扩展模块时可以被释放。
(3)多进程替代多线程
对于 CPU 密集型任务,可以考虑使用多进程而不是多线程。每个进程都有独立的 GIL,并且可以运行在不同的 CPU 核心上,从而真正实现并行计算。
python
from multiprocessing import Process
def cpu_task():
total = 0
for i in range(10000000):
total += i
print(total)
processes = []
for i in range(4):
p = Process(target=cpu_task)
processes.append(p)
p.start()
for p in processes:
p.join()
(4)异步编程
除了多线程,Python 还支持异步编程(asyncio
),尤其适用于 I/O 密集型任务。异步编程通过事件循环和协程来管理任务的并发执行,避免了线程上下文切换的开销和 GIL 的影响。
python
import asyncio
async def async_task():
print("Starting async task")
await asyncio.sleep(2) # 模拟异步I/O操作
print("Async task finished")
async def main():
tasks = [async_task() for _ in range(4)]
await asyncio.gather(*tasks)
asyncio.run(main())
参考文献
[1] https://docs.python.org/zh-cn/3/library/asyncio.html
[2] https://docs.python.org/zh-cn/3/library/multiprocessing.html
[3] No GIL Python 的冒险:https://www.4async.com/2024/03/adventure-of-no-gil-python/
[4] Python 的 GIL 是什么鬼,多线程性能究竟如何:https://cenalulu.github.io/python/gil-in-python/
[5] Understanding the Python GIL:https://www.dabeaz.com/GIL/
NLP工程化(星球号)