去年有个项目,我需要用多进程处理一批图片文件。在Windows笔记本上写完代码,测试跑得很顺畅,于是信心满满地提交了代码。结果CI环境(Linux)直接报错:
NameError: name 'config' is not defined我盯着报错信息看了十分钟,脑子里只有一个念头:config明明定义在模块顶层了,所有函数都能访问,为什么子进程说找不到?
更让我崩溃的是,同样的代码在Windows上跑得好好的,上了Linux反而崩了。这完全颠覆了我的认知。
后来我才发现: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作为参数传进去不就行了?”
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。
# 这样写也不行
def wrapper(data):
return process_data(data, config)
with Pool(4) as pool:
results = pool.map(wrapper, data_list) # wrapper是嵌套函数,无法pickle这就是为什么“明明传了参数,进程池却说没找到”——不是参数没传,而是函数本身无法被子进程识别。
if __name__之后定义再看一段代码:
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__之外):
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)functools.partial固定参数(推荐)partial可以固定函数的部分参数,而且它是可pickle的:
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,子进程拿到后可以正常调用。
Pool的initializer初始化全局变量如果多个worker都需要同一个配置,可以在每个子进程启动时通过initializer初始化:
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设置到子进程的全局变量中。
starmap传递多个参数如果你的函数需要多个参数,用starmap比map更合适:
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 | ✅ 是 | ✅ | 多参数函数 |
我那个图片处理程序最后用了functools.partial。把配置用partial固定后传给pool.map,Windows和Linux都跑得稳稳的。
记住:在Python多进程里,“传了参数”不等于“子进程能拿到”。 跨平台兼容的关键是——所有传给进程池的函数和参数,都必须是可pickle的。函数必须是模块级的(不能是lambda或嵌套函数),参数必须是基本类型或可pickle的对象。
这个坑我踩了整整两天才爬出来。希望你能少花这两天。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。