为何对单个numpy数组的多线程操作能提升性能?Python内存访问疑惑
详细说明
假设要对100个numpy数组进行计算,数据存储为形状(100, 1000, 1000)的连续np.ndarray数组。遍历数据并对每个(1000, 1000)大小的块调用multiply函数,结果存储在已初始化的数组中以避免不必要的复制:
import numpy as np import time data = np.random.random((100, 1000, 1000)) result = np.zeros((100, 1000, 1000)) def multiply(source, dest): """Dummy calculation""" np.divide(source, 1.5, out=dest) start = time.time() # Call multiply on each subarray for i in range(100): multiply(data[i], result[i]) print(f"For loop exec time {time.time() - start:.3f} seconds")
在我的系统上运行输出:
>>> For loop exec time 0.381 seconds
并发实现
接下来用concurrent.futures的ThreadPoolExecutor重写代码,每次提交10个对multiply函数的异步调用,等待完成后处理所有数据:
from concurrent.futures import ThreadPoolExecutor, wait threaded_result = np.zeros((100, 1000, 1000)) executor = ThreadPoolExecutor() start = time.time() for i in range(10): futures = { executor.submit( multiply, # callable data[i*10 + j], threaded_result[i*10 + j], ) for j in range(10) } wait(futures) print(f"Threaded exec time {time.time() - start:.3f} seconds") executor.shutdown() print(f"Both equal: {np.allclose(result, threaded_result)}") # Sanity check
运行输出:
>>> Threaded exec time 0.079 seconds >>> Both equal: True
疑惑
我原本预期两种方法的运行时间大致相同,但结果出乎意料。我对numpy内部机制的理解是,传递array[index]时,实际是传递整个数组加上子数组的偏移量和大小。那当np.divide的底层实现接收到子数组data[i]和result[i]时,CPython难道不会阻止所有其他线程操作整个data和result数组吗?
性能提升是因为多线程可以读取data、将结果计算到缓冲区中再依次等待写入result,还是我对底层机制的理解存在根本性错误?
你的疑惑核心在于对CPython的GIL(全局解释器锁)和numpy底层实现的理解偏差,具体可以从以下几点解释:
GIL的作用范围与释放逻辑
CPython的GIL仅在执行Python字节码时持有,而numpy的核心运算(比如np.divide)是基于C语言实现的扩展函数。这类密集型计算函数在执行时会主动释放GIL,允许其他线程同时执行同类C扩展运算。也就是说,你的多线程调用中,多个np.divide操作可以并行执行,不会被GIL阻塞。子数组的内存访问无冲突
虽然data[i]是原数组的视图(共享底层内存),但每个线程处理的data[i]和threaded_result[i]对应的内存区域完全不重叠:data[i]对应原数组中从i*1000*1000到(i+1)*1000*1000的内存块,不同i的块之间没有交集。读取无重叠的内存区域不需要锁,写入无重叠的区域也不会产生竞争条件,因此多线程操作是安全的,无需互相阻塞。性能提升的本质
正因为numpy释放了GIL,且各线程的内存操作无冲突,多线程版本可以充分利用CPU的多核优势并行执行计算,所以运行时间远低于单线程循环。而单线程版本只能串行处理每个子数组,无法发挥多核性能。
总结来说,你的理解误区在于认为CPython会阻止所有对同一数组的多线程访问,但实际上GIL的限制仅针对Python字节码,numpy的C扩展会绕过这个限制,再加上子数组的内存无重叠,所以多线程能够高效并行。
内容的提问来源于stack exchange,提问作者simeonovich

