Python的全局锁(GIL)是 CPython 解释器实现中的一个机制,用来确保任何时候只有一个线程执行 Python 字节码。这一机制存在于 CPython 中,主要是为了确保线程操作中的数据一致性,但也因此限制了多线程的并行执行效率。尤其是在多核 CPU 上,GIL 使得 CPU 的多核优势难以在纯 Python 的多线程程序中得到完全利用。
GIL 的基本知识
- 作用:GIL 保证在 CPython 中,同时只能有一个线程执行 Python 代码(即字节码)。
- 原因:CPython 的许多内建对象(如字典、列表等)不是线程安全的。通过使用 GIL,CPython 避免了在这些对象上加锁,从而简化了对象的内存管理。
- 影响:GIL 会使得多线程程序在多核 CPU 上无法完全并行,导致在 CPU 密集型任务中表现不佳。但是对于 I/O 密集型的任务(如网络 I/O、文件读写等),GIL 的影响相对较小。
CPython 实现 GIL 的基本原理
在 CPython 中,GIL 被实现为一个互斥锁。每次只有持有 GIL 的线程才能执行 Python 字节码,其它线程则处于等待状态。GIL 的执行流程如下:
- 一个线程持有 GIL 时,可以执行 Python 代码。
- 执行一段时间后,线程会释放 GIL,让其他线程有机会获得它(在 CPython 中通常会计数 Python 的指令)。
- 另一个线程获得 GIL 并开始执行。
简化 C++ 示例:模拟 GIL 的作用
以下代码模拟了 GIL 在多线程中的作用。它展示了如何使用一个全局互斥锁来控制线程执行的访问权限。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex gil; // 模拟 GIL
void python_thread(int thread_id) {
for (int i = 0; i < 5; ++i) {
std::lock_guard<std::mutex> lock(gil); // 获得“GIL”
std::cout << "线程 " << thread_id << " 正在执行 Python 代码, i = " << i << std::endl;
} // 离开作用域时自动释放“GIL”
}
int main() {
std::thread t1(python_thread, 1);
std::thread t2(python_thread, 2);
t1.join();
t2.join();
return 0;
}
在这个示例中,我们模拟了 GIL 的作用,通过 std::mutex
来控制每个线程对共享资源的访问。这段代码展示了线程如何在每次需要执行 Python 代码时获取 GIL,其他线程必须等待直到 GIL 被释放。
Python 中多线程的简单示例
以下 Python 代码展示了 GIL 对多线程程序的影响。在这个例子中,我们使用了 CPU 密集型的计算,观察到 GIL 的存在会导致多线程表现不如预期。
import threading
import time
def cpu_bound_task():
x = 0
for _ in range(100000000):
x += 1
print("完成计算")
# 启动两个线程执行 CPU 密集型任务
start_time = time.time()
t1 = threading.Thread(target=cpu_bound_task)
t2 = threading.Thread(target=cpu_bound_task)
t1.start()
t2.start()
t1.join()
t2.join()
end_time = time.time()
print(f"总耗时: {end_time - start_time} 秒")
在这个 Python 代码中,两个线程分别执行同样的 CPU 密集型任务,但因为 GIL 的存在,它们并不能并行运行。执行时间通常会接近单线程的 2 倍,而不是预期中的加速。
总结
- GIL 是什么:GIL 是一个全局锁,确保同一时间只有一个线程执行 Python 字节码。
- GIL 的原因:主要为了保证数据一致性,避免加锁带来的复杂性。
- GIL 的影响:GIL 限制了多核 CPU 上的 Python 多线程性能表现,尤其是在 CPU 密集型任务中;但对于 I/O 密集型任务,影响较小。
通过示例代码,我们可以看到 GIL 如何限制了多线程 Python 程序的并行性。在实际应用中,Python 程序员可以通过使用多进程(而不是多线程)来避开 GIL 的影响,或者使用如 Cython
、Numpy
等基于 C 实现的库来提高性能。