全局变量在多线程、异步协程、模块重载及from导入场景下均不安全:多线程需用threading.local(),异步应选contextvars.contextvar,模块级变量初始化须移入函数,禁用from导入可变对象。

全局变量在多线程里会“串数据”
Python 的 global 变量不是线程安全的,多个线程同时读写同一个全局变量时,结果不可预测——不是偶尔出错,而是只要并发发生,就大概率出问题。
典型现象是:函数返回值忽对忽错,日志里看到 A 用户的请求里混进了 B 用户的数据,或者计数器增长远超实际调用次数。
- 别用
global存用户上下文(比如current_user_id)、临时缓存或状态标记 - 如果必须共享状态,用
threading.local()替代:每个线程拿到的是独立副本 -
logging模块内部就靠threading.local实现 logger 实例隔离,可以照着它抄
示例:
import threading _local = threading.local() <p>def set_user_id(uid): _local.uid = uid # 各线程互不干扰</p><p>def get_user_id(): return getattr(_local, 'uid', None)
模块级变量被 reload 后行为异常
Python 中模块只导入一次,但开发时频繁用 importlib.reload(),这时模块级变量不会重置——旧值残留,新逻辑却已加载,导致“改了代码但没生效”的假象。
常见于 Flask/Django 开发中热重载、Jupyter notebook 多次运行同一 cell、或测试时反复 import 模块。
立即学习“Python免费学习笔记(深入)”;
- 避免在模块顶层写可变对象赋值,比如
cache = {}或counter = 0 - 把初始化逻辑放进函数里,每次调用才生成新实例:
def get_cache(): return {} - 真要懒加载,用
functools.cached_property或显式检查是否已初始化
异步协程共用全局状态等于裸奔
asyncio 不是多线程,但多个协程在同一线程内切换执行,共享同一份全局变量。一旦某个协程修改了 global 或模块变量,其他协程下次读取时就会拿到脏数据。
比多线程更隐蔽:没有锁报错、没有 segfault,只有逻辑错乱,且复现困难。
-
async def函数里绝对不要读写模块级可变状态 - 需要跨协程传参?走函数参数,或用
contextvars.ContextVar(Python 3.7+) -
ContextVar是 asyncio-aware 的,asyncio.create_task()会自动继承父上下文
示例:
import contextvars
request_id_var = contextvars.ContextVar('request_id', default=None)
<h1>在入口处设值</h1><p>request_id_var.set('req-123')</p><h1>任意协程里都能安全读取</h1><p>def log_request():
rid = request_id_var.get()
print(f"Handling {rid}")
from module import x 导致状态引用错乱
写 from utils import config 看似方便,但如果 config 是个可变对象(比如 dict),后续任何地方修改它,所有通过 from 导入的地方都会同步看到变化——因为大家引用的是同一个内存地址。
这比直接 import utils 更危险:后者至少能意识到“这是别人家的模块”,而前者容易误以为是本地副本。
- 禁止
from xxx import可变对象(dict,list, 自定义类实例) - 配置类优先用
dataclass+frozen=True,或封装成只读属性 - 若必须导出可变对象,文档里明确写“请勿原地修改”,并在代码里加
setattr(obj, '_frozen', True)防御性保护
容易被忽略的一点:第三方库的模块级状态(比如 requests.adapters.DEFAULT_POOLBLOCK)也会被你的代码意外污染,影响整个进程。










