基于我所阅读的内容 - 例如 here - 我了解 I/O 操作会释放 GIL。因此,如果我必须读取本地文件系统上的大量文件,我的理解是线程执行应该会加快速度。
为了对此进行测试 - 我有一个文件夹 (input
),其中包含大约 100k 个文件 - 每个文件只有一行和一个随机整数。我有两个功能 - 一个“顺序”和一个“并发”,只是将所有数字相加
import glob
import concurrent.futures
ALL_FILES = glob.glob('./input/*.txt')
def extract_num_from_file(fname):
#time.sleep(0.1)
with open(fname, 'r') as f:
file_contents = int(f.read().strip())
return file_contents
def seq_sum_map_based():
return sum(map(extract_num_from_file, ALL_FILES))
def conc_sum_map_based():
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
return sum(executor.map(extract_num_from_file, ALL_FILES))
虽然这两个函数给我相同的结果 - “并发”版本慢了大约 3-4 倍。
In [2]: %timeit ss.seq_sum_map_based()
3.77 s ± 50.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [3]: %timeit ss.conc_sum_map_based()
12.8 s ± 240 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
我的代码或我的理解有问题吗?
最佳答案
注意:以下内容仅适用于 HDD,HDD 具有可影响读取吞吐量的移动部件,不适用于 SDD。巨大性能差异的性质让我清楚地知道这是一个面向 HDD 的问题,因此此信息是在该假设下运行的。
问题在于,虽然线程可能并行运行,但数据必须从硬盘驱动器顺序读取,因为只有一个读头。然而,更糟糕的是,由于您已经并行化了 I/O 操作,底层操作系统将安排这些 I/O 任务,以便在切换到另一个线程之前仅部分处理这些文件——毕竟,即使您只有一个整数,文件头仍然需要处理——导致读取头比在严格顺序代码中跳得更疯狂。与不需要那么多跳转的简单地按顺序读取每个文件的全部内容相比,所有这些都会导致开销大大增加。
例如,如果您有一个线程从磁盘加载大量数据,而第二个线程对它执行一些时间密集型处理,那么这就不是什么大问题,因为这样可以节省时间- 密集处理继续不受 I/O 操作阻塞。您的特定场景只是一个非常非常糟糕的情况,您放弃了 GIL 瓶颈以换取极其缓慢的 I/O 瓶颈。
简而言之,您已经正确地理解了 I/O 操作释放 GIL,您只是对并行文件读取得出了错误的结论。
关于python - 文件 I/O 操作会释放 Python 中的 GIL 吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70509732/