多线程读CSV几乎无效,因pandas.read_csv是CPU密集型且受GIL限制,磁盘I/O常为瓶颈;推荐用ProcessPoolExecutor进程并行或polars+ThreadPoolExecutor实现真正并发。

多线程对读取 CSV 文件几乎没加速效果,甚至更慢——因为 pandas.read_csv 是 CPU 密集型操作,且底层 C 实现已高度优化,Python 多线程受 GIL 限制无法并行执行;同时磁盘 I/O 在多数情况下是瓶颈,线程切换反而增加开销。
为什么 threading 读 CSV 基本无效
常见现象:启 8 个线程读 100 个 CSV,总耗时比单线程还长 20%~50%。根本原因有三:
-
read_csv的解析逻辑(如类型推断、分隔符处理)由 C 代码完成,GIL 在调用这些 C 函数时仍被持有,线程实际是串行执行 - 频繁的线程上下文切换消耗 CPU 时间,尤其当文件小、数量多时,开销占比更高
- 机械硬盘(HDD)随机读性能极差,多线程并发读会加剧磁头寻道抖动;即使是 SSD,文件系统缓存和队列策略也可能导致竞争而非协同
真正有效的替代方案:用 concurrent.futures.ProcessPoolExecutor
绕过 GIL 的唯一可靠方式是进程级并行。但要注意:不能直接把 pd.read_csv 丢进子进程——pandas 对进程间数据序列化支持有限,且每个子进程都会加载完整 pandas,内存开销陡增。
使用 qbo-mileage CLI 及用户凭证,从 Airtable、Outlook 或 Google Calendar 记录生成 QuickBooks Online 里程 CSV 文件。
- 推荐做法:在子进程中只做「原始读取 + 类型预设」,避免自动类型推断(
dtype='string'或显式指定列类型) - 关闭索引生成:
index_col=False,减少 pandas 内部对象构建开销 - 慎用
chunksize:它返回迭代器,在多进程里需手动拼接,易出错;不如一次性读完再返回DataFrame - 示例关键片段:
from concurrent.futures import ProcessPoolExecutor
import pandas as pd
def load_csv(path):
return pd.read_csv(path, dtype='string', index_col=False)
with ProcessPoolExecutor(max_workers=4) as executor:
dfs = list(executor.map(load_csv, file_paths))
更轻量、更可控的选择:用 polars + ThreadPoolExecutor
如果必须用线程(例如受限于内存、无法 fork 进程),polars 是目前唯一能在线程中真正并发读 CSV 的 Python 库——它的 Rust 后端不依赖 GIL,且默认启用多线程解析(无需额外配置)。
立即学习“Python免费学习笔记(深入)”;
- 安装:
pip install polars - 直接用线程池提交
pl.read_csv,每个调用都可利用多个 CPU 核心解析自身文件 - 注意路径必须是字符串(
Path对象需先转str) - 避免混合使用 pandas 和 polars 的 DataFrame,类型转换开销可能抵消收益
- 示例:
import polars as pl
from concurrent.futures import ThreadPoolExecutor
def load_pl(path):
return pl.read_csv(path)
with ThreadPoolExecutor(max_workers=8) as pool:
lf_list = list(pool.map(load_pl, file_paths))
真正卡点往往不在“选线程还是进程”,而在于是否提前约束了列类型、是否关闭了冗余功能(如 index、infer_types)、以及是否误以为“更多 worker = 更快”——超过物理核心数的进程/线程只会加剧调度争抢。实测中,4~6 个 worker 通常是吞吐拐点,再往上收益趋近于零。

















