在上文《Python 线程基础》提到了Python的GIL机制,本文进行进一步介绍。
Python 的 GIL(Global Interpreter Lock,全局解释器锁)是 CPython 解释器中的一个机制,用于确保同一时刻只有一个线程执行 Python 字节码。
可以从三个方面理解 GIL 的作用和影响:
- 多线程并发对CPU密集型任务无效。
- 多线程并发对IO密集型任务有效。
- 优化CPU密集型任务: 使用多进程、使用优化过的三方库。
1 多线程并发对CPU密集型任务无效
先看示例1,这个示例是对两个矩阵进行乘法运算。
import numpy as np
import time
def matrix_multiply(n, seed=0, dtype=np.float32) -> np.ndarray:
"""
计算两个 n x n 随机矩阵的乘积,并返回结果。
参数:
n (int): 矩阵的维度(n x n)
seed (int or None): 随机种子,用于复现结果;设为 None 则不固定
dtype (np.dtype): 矩阵数据类型,默认 float32(节省内存)
返回:
result (np.ndarray): 结果矩阵 (n x n)
"""
if seed is not None:
np.random.seed(seed)
matrix1 = np.random.rand(n, n).astype(dtype)
matrix2 = np.random.rand(n, n).astype(dtype)
result = np.zeros((n, n), dtype=dtype)
for i in range(n):
for j in range(n):
total = 0
for k in range(n):
total += matrix1[i][k] * matrix2[k][j]
result[i][j] = total
return result
if __name__ == "__main__":
n = 128
start_time = time.perf_counter() # 更准确的计时器
matrix_multiply(n)
end_time = time.perf_counter()
elapsed = end_time - start_time
print(f"完成计算,花费时间是 {elapsed:.2f} 秒")
示例1的运行时间大约为0.7秒。
接下来看示例2,示例2使用三个线程同时执行matrix_multiply()。
import threading
import numpy as np
import time
def matrix_multiply(n, seed=0, dtype=np.float32) -> np.ndarray:
...
if __name__ == "__main__":
n = 128
start_time = time.perf_counter() # 更准确的计时器
threads = []
for i in range(3):
t = threading.Thread(target=matrix_multiply, args=(n,))
t.start()
threads.append(t)
for t in threads:
t.join()
end_time = time.perf_counter()
elapsed = end_time - start_time
print(f"完成计算,花费时间是 {elapsed:.2f} 秒")
示例2的运行时间大约为2.1秒,也就是说,由于 GIL 机制,虽然使用了三个线程进行并发,并没有并发效果,实际上三个线程还是串行运行。
2 多线程并发对IO密集型任务有效
Python 在进行“阻塞式 I/O”时会释放 GIL,使得多线程对阻塞 I/O 密集型任务有效。对于“非阻塞 I/O”,多线程无意义甚至是负优化,应使用事件循环(asyncio)。
看一个示例,通过多线程下载网络内容。
import time
import requests
from concurrent.futures import ThreadPoolExecutor
# 5个网站
urls = [
'https://www.qq.com',
'https://www.qq.com',
'https://www.qq.com',
'https://www.qq.com',
'https://www.qq.com',
]
def download(url) -> int:
response = requests.get(url, timeout=10)
return len(response.content)
if __name__ == "__main__":
# 单线程
print("单线程下载:")
start = time.time()
for url in urls:
size = download(url)
print(f" {url}: {size} bytes")
single_time = time.time() - start
print(f"耗时: {single_time:.2f} 秒
")
# 多线程
print("多线程下载:")
start = time.time()
with ThreadPoolExecutor(max_workers=len(urls)) as executor:
results = executor.map(download, urls)
for url, size in zip(urls, results):
print(f" {url}: {size} bytes")
multi_time = time.time() - start
print(f"耗时: {multi_time:.2f} 秒
")
# 对比
print(f"性能对比: {single_time/multi_time:.2f}x")
上述示例中,单线程耗时约为1.25秒,多线程耗时约为0.25秒。
3 CPU密集型任务的优化
3.1 使用多进程进行并发
from concurrent.futures import ProcessPoolExecutor
from multiprocessing import Pool
def compute(n):
return sum(i*i for i in range(n))
if __name__ == '__main__':
# 多进程方案1: multiprocessing
with Pool(processes=4) as pool:
results = pool.map(compute, [1000000] * 8)
# 多进程方案2: multiprocessing的高级封装
with ProcessPoolExecutor(max_workers=4) as executor:
results = executor.map(compute, [1000000] * 8)
3.2 使用优化过的三方库
Python中的一些三方库针对科学计算进行过专门的优化,应该优先使用这些库,而不是使用纯粹的Python代码。
import time
import numpy as np
def matrix_multiply_numpy(n, seed=0, dtype=np.float32):
"""使用NumPy的 @ 或 dot 运算符"""
if seed is not None:
np.random.seed(seed)
matrix1 = np.random.rand(n, n).astype(dtype)
matrix2 = np.random.rand(n, n).astype(dtype)
# 方法1: @ 运算符(推荐)
result = matrix1 @ matrix2
# 方法2: np.dot(等价)
# result = np.dot(matrix1, matrix2)
# 方法3: np.matmul(等价)
# result = np.matmul(matrix1, matrix2)
return result
if __name__ == "__main__":
n = 128
# 单线程
start_time = time.perf_counter()
matrix_multiply_numpy(n)
end_time = time.perf_counter()
elapsed = end_time - start_time
print(f"完成计算,花费时间是 {elapsed:.2f} 秒")
上述示例中,matrix_multiply_numpy()的执行时间约为 0.03 秒,刚开始的示例函数matrix_multiply()的执行时间约为 0.7 秒,性能提升了 23 倍左右。
4 总结
Python GIL 会被释放的主要场景: 执行阻塞I/O操作;某些高性能三方库执行CPU密集计算;调用 time.sleep();多线程中,等待线程通过5ms超时机制触发GIL释放请求,持有线程响应该请求释放GIL (也就是说,GIL可以保证同一时刻只有一个线程执行 Python 字节码,但是这个线程是可以切换的)。