Polars 多线程中 to_pandas 卡住,另一个任务正在 map_elements
Polars 导出 pandas 与 Python UDF 并发执行时,GIL 和 Rayon 线程池可能互相等待。核对具体调用栈与分块条件,优先改用原生表达式或有边界地串行化两条路径。
一组 Python 线程同时处理 Polars 数据,进程没有退出,to_pandas() 却一直不返回。若另一个任务正在执行带 Python 函数的 map_elements(),要检查 GIL 与 Polars 内部线程池的互相等待;这与单纯导出数据量太大不同。
Polars 1.44.2、2.0.0 和 2.1.0 都出现过这项并发等待。实际批处理可能先经过 unique()、streaming collect,产生多个数据块,再把结果交给导出任务。只用一个很小的单块 DataFrame 测试,未必能覆盖相同路径。
Python 线程和内部线程池都可能在等
导出阶段可能持有 GIL,等待内部 Rayon 工作线程完成数据块转换;另一项并发操作已经占用工作线程,执行 Python UDF 时又需要 GIL。两边形成等待环,增加 Python 外层线程数或内部线程数只能改变碰到它的机会,不能保证解开。
在隔离环境保存线程栈、Polars 与 Python 版本、输入 dtype、块数量和并发入口。先区分持续工作但很慢、I/O 等待,以及没有进展的锁等待。提前导入 pandas、pyarrow 可能排除导入期间的干扰,却不是这项运行期等待的修复。
不要在生产任务卡住后继续无限提交新批次。暂停接收或让调度器执行既有超时退出流程,保留未完成任务标识;强制结束进程可能中断其他写入,需要按原有任务恢复机制确认结果,不能直接将所有超时任务当作未执行。
能表达为 Polars 运算时,移除 Python UDF
例如简单算术不需要逐元素回到 Python:
import polars as pl
result = frame.with_columns((pl.col("value") + 1).alias("value"))
frame 使用原数据;示例只替代“每个值加一”的 UDF。复杂函数不能随意改成这段表达式,应核对 null、异常值、返回类型和原函数语义。原生表达式也通常能避免逐元素 Python 调用。
必须保留 Python 函数时,可暂时让 UDF 阶段与 to_pandas() 导出阶段顺序执行。若采用同一把锁保护,应覆盖应用所有会走这两种路径的任务,而不是只锁导出这一侧;锁内不要再等待另一个必须取得该锁的任务。串行化会降低并行度,需要明确队列上限和等待时间。
更新后保留原来的并发与分块条件
2.1.0 的 to_pandas 仍有在持有 GIL 的路径上迭代数据块的逻辑。更新后要保留原来的并发与分块条件做检查;单线程导出成功,不能说明两条路径同时运行时也能结束。
验收使用原来会产生多个块的转换顺序,并让 UDF 与导出确实并发,观察所有任务在规定时间内结束。另核对行顺序、dtype、null 和输出结果,避免用删除 UDF 逻辑或丢掉任务来换取“不再卡住”。回退版本时,应继续保留已经确认必要的并发隔离。


