单纯靠 threading.Lock 无法解决竞态,真正优化需聚焦锁的范围与操作是否真需锁;粗粒度锁会拖慢程序,应按数据边界细粒度加锁,避免在I/O或阻塞调用中持锁,且须区分GIL与Lock作用——GIL不保证共享变量安全。

单纯靠 threading.Lock 无法解决竞态,反而可能掩盖更深层的并发设计缺陷;真正有效的优化必须从“锁的范围”和“操作是否真需要锁”两个维度入手。
为什么用粗粒度 Lock 反而拖慢程序
常见误区是把整个函数或一大段逻辑包进一个 with lock: 块里。这会让线程频繁排队、空等,尤其在 I/O 或计算耗时较长时,实际吞吐量可能比单线程还低。
- 比如对一个全局字典做频繁读写,却用一把锁保护整个字典——读操作本可并发,却被强制串行
-
time.sleep()、requests.get()、open()等阻塞调用放在锁内,会白白占用锁资源,其他线程只能干等 - GIL 在 I/O 期间会释放,但你的
Lock不会——它只认你手动加/放,跟 GIL 无关
细粒度锁:按数据边界而非代码边界加锁
核心原则:只锁真正共享且非原子的写操作,读操作尽量不锁,写操作按最小可隔离单元分锁。
- 用
threading.RLock替代Lock仅当需在同一线程内递归进入临界区(如递归函数修改共享状态) - 对字典类结构,可用
collections.defaultdict(lambda: threading.Lock())为每个 key 分配独立锁,避免“一锁锁全表” - 若共享对象是列表,不要锁整个 list,而是按索引范围分段加锁(例如每 100 个元素共用一把锁)
- 注意:锁对象本身不能是临时变量,必须是模块级或实例属性,否则每次新建等于没锁
哪些操作其实根本不用锁?
不是所有共享访问都导致竞态。Python 中部分操作天然具备原子性或线程安全语义,硬加锁纯属浪费。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
-
list.append()、dict[key] = value(key 已存在)、queue.Queue.put()是 CPython 解释器保证的原子操作,无需额外锁 -
counter += 1不是原子的——它等价于counter = counter + 1,含读、算、写三步,必须锁 -
threading.local()创建的变量天生线程隔离,完全规避共享,适合存请求上下文、数据库连接等 - 如果只是往
logging.getLogger().info()写日志,logging模块内部已用锁保护,外部再套一层锁毫无意义
容易被忽略的坑:锁和 GIL 不是一回事
很多人以为“Python 有 GIL,所以多线程不用怕竞态”,这是危险错觉。GIL 只管 Python 字节码执行权,不管你的变量怎么被读写。
- GIL 会在
time.sleep()、socket.recv()、file.read()等系统调用前释放,此时多个线程可同时运行,共享变量暴露在无保护状态 - 用 C 扩展(如 NumPy 数组运算)时,GIL 往往被主动释放以提升性能,但你的 Python 层变量若未加锁,就在这段“无 GIL 时间窗”里被并发修改
-
threading.Lock是用户级同步原语,它不依赖 GIL,也不受 GIL 调度影响——它只听你acquire()和release()的指令
最常出问题的地方,是混合了 I/O 等待和共享状态更新的逻辑,既没意识到 GIL 已释放,又忘了自己该上锁。这种场景下,竞态不是“偶尔发生”,而是“必然发生”,只是表现时机难复现。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










