一个让我在Windows上彻底崩溃的下午
去年有个项目,我需要用多进程处理一批图片文件。在Windows笔记本上写完代码,测试跑得很顺畅,于是信心满满地提交了代码。结果CI环境(Linux)直接报错:
csharp
NameError: name 'config' is not defined
我盯着报错信息看了十分钟,脑子里只有一个念头:config明明定义在模块顶层了,所有函数都能访问,为什么子进程说找不到?
更让我崩溃的是,同样的代码在Windows上跑得好好的,上了Linux反而崩了。这完全颠覆了我的认知。
后来我才发现:Python多进程的参数传递,藏着一个惊天大坑------你传给进程池的函数,在子进程里可能根本"认不得"你在主进程里定义的变量。
第一幕:一个"明明传了参数"的诡异报错
先看一段"看起来完全正确"的代码:
python
from multiprocessing import Pool
config = {"mode": "fast", "retry": 3}
def process_data(data):
# 用config处理数据
return data * config["retry"]
if __name__ == "__main__":
data_list = [1, 2, 3, 4, 5]
with Pool(4) as pool:
results = pool.map(process_data, data_list)
print(results)
这段代码在Linux/macOS下(默认fork方式)通常能正常运行。子进程会复制父进程的整个内存空间,config变量被一起复制过去了。
但如果在Windows 下运行,或者Linux下切换到了spawn方式,就会报错NameError: name 'config' is not defined。
为什么?
Windows没有fork系统调用,Python只能使用spawn方式启动子进程------启动一个全新的Python解释器 ,重新执行所有导入代码。子进程里的全局变量是重新初始化后的值,父进程里定义的config,子进程根本不知道。
更坑的是,Python 3.8开始在macOS上默认也切换到了spawn。也就是说,同样的代码,在不同的操作系统、甚至不同的Python版本下,行为可能完全不一样。
第二幕:为什么"传了参数"还是找不到?
有人可能会说:"那我把config作为参数传进去不就行了?"
python
from multiprocessing import Pool
def process_data(data, config):
return data * config["retry"]
if __name__ == "__main__":
config = {"mode": "fast", "retry": 3}
data_list = [1, 2, 3, 4, 5]
with Pool(4) as pool:
# 把config作为参数传进去
results = pool.map(lambda x: process_data(x, config), data_list)
print(results)
这段代码在Windows上依然会报错。原因有两个:
原因一:lambda函数无法被pickle
multiprocessing需要在进程之间传递参数和函数,它使用pickle序列化数据。但lambda函数无法被pickle 。当你把lambda x: process_data(x, config)传给pool.map时,Python尝试pickle这个lambda,直接报错。
原因二:嵌套函数同样无法被pickle
在Windows(spawn方式)下,pool.map要求传入的函数必须是模块级别的全局函数。嵌套函数、lambda、实例方法都无法被pickle。
python
# 这样写也不行
def wrapper(data):
return process_data(data, config)
with Pool(4) as pool:
results = pool.map(wrapper, data_list) # wrapper是嵌套函数,无法pickle
这就是为什么"明明传了参数,进程池却说没找到"------不是参数没传,而是函数本身无法被子进程识别。
第三幕:一个更隐蔽的坑------变量在if __name__之后定义
再看一段代码:
python
from multiprocessing import Pool
def worker(x):
return x * multiplier
if __name__ == "__main__":
multiplier = 10 # 在if块里定义
with Pool(2) as pool:
results = pool.map(worker, [1, 2, 3])
print(results)
在Windows下,子进程会重新导入模块,但multiplier = 10在if __name__ == "__main__"块里面。子进程的__name__不是"__main__",所以这行代码根本不会执行 。子进程里的multiplier从未被定义。
这就是为什么有人说"在if __name__之后定义的值不会传递到子进程里" 。
正确的做法是 :把需要在子进程中使用的全局变量定义在模块顶层(if __name__之外):
python
from multiprocessing import Pool
multiplier = 10 # 定义在模块顶层
def worker(x):
return x * multiplier
if __name__ == "__main__":
with Pool(2) as pool:
results = pool.map(worker, [1, 2, 3])
print(results)
第四幕:正确传递参数的3种姿势
姿势1:用functools.partial固定参数(推荐)
partial可以固定函数的部分参数,而且它是可pickle的:
python
from multiprocessing import Pool
from functools import partial
def process_data(data, config):
return data * config["retry"]
if __name__ == "__main__":
config = {"mode": "fast", "retry": 3}
process_with_config = partial(process_data, config=config)
with Pool(4) as pool:
results = pool.map(process_with_config, [1, 2, 3, 4, 5])
print(results)
partial返回的对象可以被pickle,子进程拿到后可以正常调用。
姿势2:用Pool的initializer初始化全局变量
如果多个worker都需要同一个配置,可以在每个子进程启动时通过initializer初始化:
python
from multiprocessing import Pool
def init_worker(config):
global global_config
global_config = config
def worker(x):
return x * global_config["retry"]
if __name__ == "__main__":
config = {"mode": "fast", "retry": 3}
with Pool(4, initializer=init_worker, initargs=(config,)) as pool:
results = pool.map(worker, [1, 2, 3, 4, 5])
print(results)
initializer会在每个子进程启动时执行,把config设置到子进程的全局变量中。
姿势3:直接用starmap传递多个参数
如果你的函数需要多个参数,用starmap比map更合适:
ini
from multiprocessing import Pool
def process_data(data, retry, mode):
return data * retry
if __name__ == "__main__":
config = {"mode": "fast", "retry": 3}
# 把每个数据点和配置打包成元组
tasks = [(x, config["retry"], config["mode"]) for x in [1, 2, 3, 4, 5]]
with Pool(4) as pool:
results = pool.starmap(process_data, tasks)
print(results)
starmap接受一个元组列表,每个元组解包后作为参数传给函数。
一张表总结
| 传递方式 | 是否可pickle | 跨平台兼容性 | 推荐场景 |
|---|---|---|---|
全局变量(fork下) |
不适用 | ❌ 仅Linux/macOS | 不推荐跨平台 |
lambda表达式 |
❌ 否 | ❌ | 永远不要用 |
| 嵌套函数 | ❌ 否 | ❌ | 永远不要用 |
| 模块级函数 + 全局变量 | ✅ 是(需定义在顶层) | ⚠️ 需注意初始化时机 | 简单场景 |
functools.partial |
✅ 是 | ✅ | 最推荐 |
Pool(initializer=...) |
✅ 是 | ✅ | 需要初始化复杂状态 |
pool.starmap |
✅ 是 | ✅ | 多参数函数 |
回到开头的Bug
我那个图片处理程序最后用了functools.partial。把配置用partial固定后传给pool.map,Windows和Linux都跑得稳稳的。
记住:在Python多进程里,"传了参数"不等于"子进程能拿到"。 跨平台兼容的关键是------所有传给进程池的函数和参数,都必须是可pickle的。函数必须是模块级的(不能是lambda或嵌套函数),参数必须是基本类型或可pickle的对象。
这个坑我踩了整整两天才爬出来。希望你能少花这两天。