Python 多进程数据传递中的序列化问题解析

在 Python 中,当使用 multiprocessing 模块(如 ProcessPool)或 concurrent.futures.ProcessPoolExecutor 向子进程传递数据时,这些数据需要通过 pickle 序列化后传输。因此,只有 可序列化(pickleable) 的对象才能被安全地传递给子进程。

什么是可序列化(pickleable)?

可序列化对象 是指能被 Python 的 pickle 模块成功序列化(转换为字节流)和反序列化(从字节流恢复)的对象。

常见的可序列化类型

类型 示例 说明
基本数据类型 int, float, str, bool, None 最基础的可序列化类型
容器类型 list, tuple, dict, set, frozenset 只要其中元素可序列化即可
自定义类实例 MyClass() 前提是:类在模块顶层定义,且没有不可序列化的属性
函数(仅限全局函数) def my_func(): ... 必须是模块顶层定义的函数,不能是局部函数或 lambda
内置函数/方法 len, print, math.sqrt 部分内置函数可序列化(但不推荐依赖其可移植性)
数值库对象 numpy.ndarray(小数组) NumPy 数组通常可序列化,但大数组效率低
pathlib.Path Path('/tmp/file.txt') Python 3.6+ 支持
datetime 对象 datetime.datetime.now() 标准库中的时间对象可序列化

示例(可序列化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import pickle

# 可序列化:嵌套容器
data = {
'name': 'Alice',
'age': 30,
'scores': [85, 92, 78],
'active': True,
'config': {'debug': False, 'timeout': 10}
}

serialized = pickle.dumps(data) # 成功
deserialized = pickle.loads(serialized) # 成功
print(deserialized)

# 自定义类(可序列化)
class Person:
def __init__(self, name):
self.name = name

p = Person("Bob")
serialized = pickle.dumps(p) # 成功

# 全局函数(可序列化)
def worker(x):
return x * 2

from multiprocessing import Pool
with Pool() as pool:
result = pool.map(worker, [1, 2, 3]) # 正常工作

什么是不可序列化(not pickleable)?

不可序列化对象 是那些 pickle 无法保存其状态或行为的对象,通常是包含运行时上下文、内存地址、系统资源、闭包等的对象。

常见的不可序列化类型

类型 示例 原因
lambda 函数 lambda x: x + 1 无名称,无法被 pickle 找到定义
嵌套函数 / 局部函数 在另一个函数内部定义的函数 无法被模块级导入
类方法(绑定方法) obj.method 绑定到特定实例,pickle 无法重建绑定关系
生成器 (x for x in range(10)) 状态依赖执行上下文,无法捕获完整状态
文件对象 open('file.txt') 持有操作系统文件描述符,不可跨进程传递
线程/锁/事件等同步原语 threading.Lock(), threading.Event() 与特定线程/进程绑定,跨进程无效
数据库连接 sqlite3.connect(...) 持有 socket 或本地句柄
网络连接/套接字 socket.socket() 无法在进程间共享底层资源
闭包中引用的非全局变量 def outer(): x=1; def inner(): return x; return inner 闭包环境(cell 对象)无法被 pickle 持久化
某些第三方库对象 cv2.VideoCapture(...), pygame.Surface 内部封装 C 资源,无 Python 级序列化支持
动态创建的类 type('DynamicClass', (), {})() 不在模块命名空间中,pickle 无法定位反序列化路径
含有不可序列化属性的类实例 class A: def __init__(self): self.f = lambda x: x 属性是 lambda → 整个对象不可序列化

示例(不可序列化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import pickle
from multiprocessing import Process

# Lambda 函数
f = lambda x: x + 1
try:
pickle.dumps(f) # 抛出 PicklingError
except Exception as e:
print("Lambda 不可序列化:", e)

# 局部函数
def make_func():
def local_func(x):
return x * 3
return local_func

g = make_func()
try:
pickle.dumps(g) # 抛出 PicklingError
except Exception as e:
print("局部函数不可序列化:", e)

# 文件对象
with open('test.txt', 'w') as f:
try:
pickle.dumps(f) # 抛出错误
except Exception as e:
print("文件对象不可序列化:", e)

# threading.Lock
import threading
lock = threading.Lock()
try:
pickle.dumps(lock) # 抛出错误
except Exception as e:
print("Lock 不可序列化:", e)

# 包含 lambda 的类
class BadClass:
def __init__(self):
self.handler = lambda x: x + 10

obj = BadClass()
try:
pickle.dumps(obj) # 失败!因为 self.handler 是 lambda
except Exception as e:
print("含 lambda 的对象不可序列化:", e)

如何解决不可序列化问题?

方法一:改用全局函数

错误做法(使用 lambda):

1
pool.map(lambda x: x**2, data)

正确做法(定义全局函数):

1
2
3
4
def square(x):
return x ** 2

pool.map(square, data)

方法二:避免在类中存储不可序列化对象

不推荐

1
2
3
class Worker:
def __init__(self):
self.lock = threading.Lock() # 不可序列化

推荐做法:只传递必要数据,在子进程中初始化资源:

1
2
3
def worker_task(data):
lock = threading.Lock() # 在子进程中创建
# ... 使用 lock 处理 data

方法三:使用 dill 替代 pickle(高级场景)

dillpickle 的增强版,支持更多对象(如 lambda、嵌套函数),但不保证跨平台/跨 Python 版本兼容性,生产环境慎用:

1
pip install dill
1
2
3
4
import dill

f = lambda x: x + 1
serialized = dill.dumps(f) # 成功

注意multiprocessing 默认使用 pickle,无法直接替换为 dill。若需使用,必须重写底层序列化机制,例如借助 multiprocess 库(第三方,非标准库)。

方法四:将数据提取出来,只传递“纯数据”

错误做法(传递整个对象):

1
worker(MyDatabase())  # 包含连接句柄

正确做法(仅传递配置参数):

1
2
worker(db_config={'host': 'localhost', 'port': 5432})
# 子进程中:conn = connect(**db_config)

可序列化 vs 不可序列化速查表

类型 是否可序列化 说明
int, str, float, bool, None 基础类型
list, dict, tuple, set(内含可序列化元素) 容器递归检查
全局函数(模块顶层定义) def func(): ...
自定义类(无不可序列化属性) 类必须可通过 sys.modules 定位
numpy.array 小数组无问题,大数组慢且易 OOM
datetime 对象 标准库支持
lambda 表达式 __name__,无法反序列化定位
局部函数 / 嵌套函数 作用域不在模块顶层
threading.Lock, Event, Queue 与进程绑定,跨进程无效
文件对象、套接字、数据库连接 持有 OS 资源句柄
生成器 状态不可保存
类方法(绑定方法) obj.methodmethod 对象,非函数
动态类(type(...) 创建) 无模块路径,pickle 无法 find_class
lambda 的类实例 即使类本身可序列化,属性不可则整体失败

最佳实践建议

  1. 优先传递纯数据:字典、列表、字符串、数字等,避免复杂对象。
  2. 避免跨进程共享状态:进程间是隔离的,应通过输入-输出通信,而非共享内存(除非明确使用 multiprocessing.shared_memory)。
  3. **全局函数优于局部函数或 **lambda:确保函数在模块顶层定义且可导入。
  4. 在子进程中初始化资源:如数据库连接、文件句柄、锁等,不要从主进程传递。
  5. 调试技巧:使用 pickle.dumps(obj) 提前验证:
1
2
3
4
5
6
7
import pickle

try:
pickle.dumps(your_object)
print("可序列化")
except Exception as e:
print("不可序列化:", e)

该检查可快速定位问题,避免在多进程环境中出现难以调试的 RuntimeError: Cannot pickle... 崩溃。