Flask多进程下单例重复初始化是因为每个Gunicorn worker独立加载应用,模块级单例仅进程内唯一;应改用外部服务(如Redis、数据库连接池)或进程间协调机制实现真正单例。

Flask 多进程下单例对象为何会重复初始化
因为每个 worker 进程都独立加载应用,__init__.py 或模块顶层代码会被各进程分别执行一次。如果你用常规方式(比如模块级变量 + if not instance: instance = MyClass())实现单例,那每个进程都有自己的 instance,根本不是“单例”,只是“每进程单例”。
典型表现:你在单例里缓存了数据库连接、Redis 客户端或配置解析结果,但不同请求路由到不同进程时,发现连接没复用、缓存不一致、甚至初始化逻辑被反复触发(比如日志里出现多次“Loading config…”)。
用 multiprocessing.Manager 共享基础数据结构
适用于需要跨进程共享简单状态(如计数器、开关标志、字典缓存),且不涉及复杂对象或 I/O 资源的情况。它底层通过子进程通信实现,不是内存共享,有延迟和序列化开销。
- 只适合
dict、list、Value、Array等支持代理的类型,不能直接托管自定义类实例 - 必须在
if __name__ == '__main__':块或 Flask 启动前初始化Manager,不能在 request context 中创建 - 示例:共享一个计数器
from multiprocessing import Manager
manager = Manager()
shared_counter = manager.Value('i', 0)
@app.route('/inc')
def inc():
shared_counter.value += 1
return str(shared_counter.value)
把单例资源移到进程外,用服务化方式调用
最可靠也最常用的做法——别让 Flask 进程自己管单例,让它去连外部服务。比如 Redis、PostgreSQL、RabbitMQ、甚至一个轻量级 FastAPI 子服务。这样既规避了多进程问题,又天然支持横向扩展。
立即学习“Python免费学习笔记(深入)”;
- 数据库连接池(如
SQLAlchemy)本身已处理多进程安全,只需确保create_engine(..., pool_pre_ping=True) - Redis 客户端(
redis-py)是线程安全的,多进程下每个进程持有一个连接实例,但后端是同一个 Redis 实例,语义上仍是单点 - 若必须共享复杂对象(如某个硬件 SDK 的句柄),可封装为 gRPC/HTTP 服务,Flask 通过
requests或httpx调用,由该服务保证单例
用文件锁 + 序列化做进程间单例协调(慎用)
仅适用于低频初始化、能容忍阻塞、且对象可完整序列化的场景,比如启动时加载一次大模型权重并缓存到磁盘。不要用于请求中频繁访问的“单例”。
- 依赖
flock或portalocker,Windows 下行为可能不一致 - 每次访问都要反序列化,性能差;并发高时锁争用明显
- 示例伪代码:
import pickle, portalocker
def get_singleton():
with open('/tmp/my_singleton.pkl', 'rb+') as f:
portalocker.lock(f, portalocker.LOCK_EX)
try:
return pickle.load(f)
except EOFError:
obj = ExpensiveSingleton()
f.seek(0)
pickle.dump(obj, f)
f.truncate()
return obj
真正难的不是“怎么写个单例”,而是判断这个单例是否真的需要跨进程可见。多数时候,你以为的“必须单例”,其实只是没分清资源归属边界——数据库连接归数据库管,缓存归 Redis 管,配置归配置中心管。强行在 Flask 进程内搞跨进程单例,往往是在给运维埋雷。


















