fork后类变量不共享,初始指向同一物理内存页,依赖写时复制(COW)机制;原地修改可变对象触发COW分裂,重新赋值不可变对象则不触发但导致进程间状态隔离。

fork 后类变量是否共享?
不共享,但初始时指向同一物理内存页 —— 这是 copy-on-write(写时复制)机制的典型表现。Python 进程 fork 时,操作系统会为子进程创建父进程地址空间的“影子副本”,所有只读数据(包括未修改的类变量)仍共用页帧;一旦任一进程尝试写入,内核才真正复制该页。
关键点在于:Python 层面的“修改”行为是否触发底层内存写入。例如 list.append()、dict.update()、+= 等原地操作会直接改写对象内容,触发 COW;而 = 赋值若指向新对象(如 MyClass.cache = {}),则只是修改引用,不必然触发 COW(但可能让子进程丢失父进程后续对该变量的更新)。
哪些类变量修改会触发 COW 分裂?
取决于变量类型和操作方式:
-
list、dict、set等可变对象的原地修改(.append()、[key] = val、.add())—— 触发 COW,子进程看到的是 fork 时刻的快照,之后各自独立变更 - 不可变对象(
int、str、tuple)的重新赋值(MyClass.counter = MyClass.counter + 1)—— 不触发 COW(因为新建对象并重绑名称),但子进程看不到父进程后续的赋值 - 使用
+=时需特别注意:list += [1]是原地操作(触发 COW),int += 1是重新赋值(不触发 COW,但语义上仍是独立变量)
multiprocessing 模块下类变量的实际表现
当用 multiprocessing.Process 或 Pool 启动子进程时,绝大多数情况是 fork(Linux/macOS 默认),因此上述 COW 行为生效。但有几点必须确认:
- 确保没启用
spawn启动方法(multiprocessing.set_start_method('spawn')),否则子进程会重新导入模块,类变量从头初始化,完全不共享初始值 -
fork后若父进程继续修改类变量(如在子进程启动后追加日志),子进程无法感知 —— 因为它们已拥有独立副本 - 若类变量是模块级全局对象(如
logging.getLogger()返回的实例),其内部状态(如 handlers 列表)也会被 COW,导致子进程日志 handler 为空或不一致
如何安全地跨进程共享类变量状态?
不要依赖 fork 的“初始共享”假象。真正需要共享的状态,必须显式使用进程安全机制:
- 用
multiprocessing.Value或multiprocessing.Array管理简单标量或数组 - 用
multiprocessing.Manager()创建代理对象(manager.dict()、manager.list()),支持跨进程同步访问(但性能较低) - 避免在类定义中直接声明可变对象作为类变量(如
cache = {}),改用惰性初始化 + 显式共享对象(如传入Manager().dict()实例) - 若仅需子进程读取配置类变量,确保其为不可变结构(
frozenset、NamedTuple、冻结的dataclass),并在 fork 前完成全部设置
最常被忽略的一点:即使你没主动修改类变量,第三方库(比如某些 ORM 或缓存层)可能在 import 或初始化阶段悄悄往类变量里塞可变对象 —— 这些隐式写入同样会触发 COW,造成子进程行为偏离预期。









