Python GIL(全局解释器锁)的使用小结
作者:唐古乌梁海
我们通常所说的GIL,指的是全局解释器锁(Global Interpreter Lock),是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。即使在多核处理器上,使用 GIL 的解释器也只允许同一时间执行一个线程。
在Python中,GIL的存在主要是为了简化CPython解释器的实现,因为CPython的内存管理不是线程安全的。GIL可以防止并发访问Python对象,从而避免多个线程同时修改同一个对象导致的数据不一致问题。
但是,GIL也导致了一个问题:在多核CPU上,使用多线程的Python程序并不能真正地并行执行,而是通过交替执行来模拟并发。因此,对于CPU密集型的任务,使用多线程并不能提高性能,甚至可能因为线程切换的开销而降低性能。
然而,对于I/O密集型的任务(如网络请求、文件读写等),由于线程在等待I/O时会被阻塞,此时GIL会被释放,从而允许其他线程运行,因此多线程在I/O密集型任务中仍然可以提升性能。
为了克服GIL的限制,可以采用多进程(使用multiprocessing模块)来利用多核CPU,因为每个进程有自己独立的Python解释器和内存空间,因此每个进程都有自己的GIL,从而可以实现真正的并行。
什么是 GIL?
GIL(Global Interpreter Lock) 是 CPython 解释器中的一个互斥锁,它确保在任何时刻只有一个线程在执行 Python 字节码。这意味着即使在多核 CPU 上,CPython 也无法实现真正的并行线程执行。
GIL 的工作原理
+-----------------------------------------------+ | Python 进程 (单个进程) | | | | +-----------------------------------------+ | | | 全局解释器锁 (GIL) | | | | | | | | 🔒 一把锁,控制 Python 字节码执行 | | | +-----------------------------------------+ | | ↑ | | | (获取/释放) | | | | | +------------+ +------------+ +------------+| | | 线程 1 | | 线程 2 | | 线程 3 || | | | | | | || | | Python代码 | | Python代码 | | Python代码 || | | 执行中 | | 等待中 | | 等待中 || | +------------+ +------------+ +------------+| | | +-----------------------------------------------+
关键点:
- 🔒 GIL 是进程级别的锁,不是线程级别的
- 📍 同一时间只有一个线程能持有 GIL 并执行 Python 字节码
- ⏳ 其他线程必须等待 GIL 被释放
import threading
import time
def count_down(n):
while n > 0:
n -= 1
# 单线程执行
start = time.time()
count_down(100000000)
single_time = time.time() - start
# 多线程执行
start = time.time()
t1 = threading.Thread(target=count_down, args=(50000000,))
t2 = threading.Thread(target=count_down, args=(50000000,))
t1.start()
t2.start()
t1.join()
t2.join()
multi_time = time.time() - start
print(f"单线程执行时间: {single_time:.2f}秒")
print(f"双线程执行时间: {multi_time:.2f}秒")
# 你会发现多线程可能比单线程更慢!
为什么需要 GIL?
1. 简化内存管理
Python 使用引用计数进行内存管理:
import sys a = [] print(sys.getrefcount(a)) # 查看对象的引用计数 b = a print(sys.getrefcount(a)) # 引用计数增加
没有 GIL 时,多个线程同时修改引用计数会导致竞争条件:
# 伪代码演示竞争条件 # 线程1: obj.ref_count += 1 # 线程2: obj.ref_count -= 1 # 如果没有同步机制,ref_count 可能出错
2. 保护内部数据结构
Python 的很多内部数据结构(如 list、dict)不是线程安全的。
GIL 的影响
CPU 密集型任务
import threading
import time
def cpu_intensive_task():
result = 0
for i in range(10**7):
result += i * i
return result
# 测试多线程性能
def test_multithreading():
threads = []
start_time = time.time()
for _ in range(4):
t = threading.Thread(target=cpu_intensive_task)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"多线程执行时间: {time.time() - start_time:.2f}秒")
# 对比多进程
import multiprocessing
def test_multiprocessing():
processes = []
start_time = time.time()
for _ in range(4):
p = multiprocessing.Process(target=cpu_intensive_task)
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"多进程执行时间: {time.time() - start_time:.2f}秒")
# 运行测试
if __name__ == "__main__":
test_multithreading() # 可能比单线程还慢
test_multiprocessing() # 真正的并行,速度更快
I/O 密集型任务
import threading
import time
import requests
def download_site(url, session):
with session.get(url) as response:
print(f"Read {len(response.content)} from {url}")
def download_all_sites(sites):
with requests.Session() as session:
# 单线程
start_time = time.time()
for url in sites:
download_site(url, session)
print(f"单线程下载时间: {time.time() - start_time:.2f}秒")
# 多线程
start_time = time.time()
threads = []
for url in sites:
thread = threading.Thread(target=download_site, args=(url, session))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
print(f"多线程下载时间: {time.time() - start_time:.2f}秒")
# I/O 密集型任务中,多线程有明显优势
如何绕过 GIL 的限制
1. 使用多进程
from multiprocessing import Pool, cpu_count
import math
def is_prime(n):
if n < 2:
return False
for i in range(2, int(math.sqrt(n)) + 1):
if n % i == 0:
return False
return True
def find_primes_parallel(numbers):
with Pool(processes=cpu_count()) as pool:
results = pool.map(is_prime, numbers)
return results
numbers = range(1000000, 1010000)
primes = find_primes_parallel(numbers)
+------------------------+
| 主进程 (协调者) |
+------------------------+
|
+-----+-----+
| |
v v
+-------+ +-------+
| 进程1 | | 进程2 |
| | | |
| 🔒GIL | | 🔒GIL | ← 每个进程有独立的GIL
+-------+ +-------+
2. 使用 C 扩展
// primes.c
#include <Python.h>
static PyObject* find_primes_c(PyObject* self, PyObject* args) {
Py_BEGIN_ALLOW_THREADS // 释放 GIL
// 执行计算密集型任务
Py_END_ALLOW_THREADS // 重新获取 GIL
return Py_BuildValue("i", result);
}
+-----------------------+
| Python 线程 |
+-----------------------+
|
v
+-----------------------+
| 释放 GIL 的 C 扩展 | ← 在C代码中手动释放GIL
+-----------------------+
|
v
+-----------------------+
| 并行计算 (无GIL限制) |
+-----------------------+
3. 使用其他 Python 实现
- Jython: 基于 JVM,没有 GIL
- IronPython: 基于 .NET,没有 GIL
- PyPy: 有 GIL,但性能更好
+----------------+----------------+----------------+ | CPython | Jython | IronPython | | (有GIL) | (无GIL) | (无GIL) | +----------------+----------------+----------------+
4. 使用异步编程
import asyncio
import aiohttp
async def download_site_async(session, url):
async with session.get(url) as response:
content = await response.read()
print(f"Read {len(content)} from {url}")
async def download_all_sites_async(sites):
async with aiohttp.ClientSession() as session:
tasks = []
for url in sites:
task = asyncio.create_task(download_site_async(session, url))
tasks.append(task)
await asyncio.gather(*tasks)
# 运行异步任务
asyncio.run(download_all_sites_async(sites))
GIL 的优缺点
优点
- 简化 CPython 实现
- 使单线程程序更快(无锁开销)
- 更容易集成非线程安全的 C 扩展
缺点
- 限制多核 CPU 的利用率
- 对 CPU 密集型多线程程序不友好
- 可能造成性能误解
实际开发建议
# 根据任务类型选择方案:
def choose_concurrency_method(task_type, data):
if task_type == "cpu_intensive":
# 使用多进程
with multiprocessing.Pool() as pool:
return pool.map(process_data, data)
elif task_type == "io_intensive":
# 使用多线程或异步
with ThreadPoolExecutor() as executor:
return list(executor.map(process_data, data))
elif task_type == "mixed":
# 混合方案:进程池 + 线程池
pass
未来展望
Python 社区正在探索移除 GIL 的方案:
- nogil 分支:尝试移除 GIL 的实验性版本
- subinterpreters:通过多个解释器实例实现真正的并行
总结
GIL 是 CPython 的历史遗留问题,它:
- 主要影响 CPU 密集型多线程程序
- 对 I/O 密集型任务影响较小
- 可以通过多进程、C 扩展、异步编程等方式绕过
到此这篇关于Python GIL(全局解释器锁)的使用小结的文章就介绍到这了,更多相关Python GIL全局解释器锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
