juechafun/05-原子化笔记本/Python-多线程读写.md
2026-01-14 21:37:35 +08:00

185 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#领域/Python
## 一句话描述
Python 多线程读写使用安全说明。
## 详细解释
### ✅ 一、核心前置原则(所有场景通用,重中之重)
1. 本次需求的业务模型:**线程 A 仅执行【写入 / 修改】所有共享变量,线程 B 仅执行【读取】所有共享变量**,是线程安全设计的最优基础模型(无读写互冲的复杂场景)。
2. 锁的核心本质:锁保护的是「**共享变量的原子操作逻辑**」,而非「单个变量」。**所有关联的共享变量,复用同一把 `threading.Lock()` 即可**,无需为每个变量新增锁(新增锁会增加死锁风险 + 复杂度)。
3. 线程安全第一准则:**安全永远优先于极致效率**,线程不安全导致的 bug脏读 / 半更新 / 数据不一致),排查成本远大于轻微的效率损耗。
4. 阻塞的真相:读线程的「阻塞」仅发生在**抢锁的瞬间**,锁持有时间是微秒级,对「读线程持续运行」无感知影响。
### ✅ 二、`threading.Lock` 互斥锁 核心知识点(唯一锁,全场景复用)
#### 1. 核心用法(推荐写法,杜绝死锁)
使用 `with 锁对象:` 上下文管理器,自动完成「加锁→执行逻辑→解锁」,无需手动调用 `acquire()`/`release()`,即使代码报错也能正常释放锁。
```
import threading
mode_lock = threading.Lock() # 全局唯一锁,保护所有共享变量
# 所有读写共享变量的操作,均用这一把锁
with mode_lock:
# 对共享变量的核心操作:读/写/清空/赋值
pass
```
#### 2. 锁的核心作用(双保障)
- 保障数据**完整性**:不会读到「半更新的中间值」(如:写线程只改了 MODE还没改 ARGS 的状态);
- 保障数据**最新性**:强制刷新线程本地缓存,读线程能拿到主内存中写线程刚更新的最新值。
### ✅ 三、多类型共享变量 线程安全操作规范
本次涉及三类共享变量,均遵循「**临界区内操作,临界区外处理**」的规则,全部复用同一把锁,按优先级排序如下:
#### 类型 1普通基础变量如 `MODE = "initial"`
```
# 写线程-赋值(临界区内)
with mode_lock:
MODE = "running"
# 读线程-取值(临界区内)
with mode_lock:
current_mode = MODE
```
#### 类型 2可变复杂变量如 `ARGS = {"timeout":5, "retry":3}` 字典)
```
# 写线程-修改属性(临界区内)
with mode_lock:
ARGS["timeout"] = 10
# 读线程-取值(临界区内拷贝快照,避免外部篡改)
with mode_lock:
current_args = ARGS.copy()
```
#### 类型 3列表对象池核心重点 `MSG_POOL = []` 消息池)
> 本类型是本次需求的核心新增点,有专属强制规则,必须严格遵守!
```
# 写线程【强制原子操作】:清空 + 写入 必须在同一个临界区内完成
with mode_lock:
MSG_POOL.clear() # 先清空
MSG_POOL.extend(新消息列表) # 后写入
```
> 规则原因:如果「清空」和「写入」分开,会出现读线程读到**空的 MSG_POOL**,是业务绝对不允许的脏数据。
### ✅ 四、「最小临界区」设计原则 + 效率优化
#### 1. 核心结论
临界区**是否影响效率,和「覆盖多少变量」无关,只和「临界区内的操作耗时」有关**。
临界区内仅做「赋值 / 拷贝 / 清空 / 写入」等**微秒级的轻量操作**,效率影响忽略。
#### 2. 最小临界区 黄金规则(最优实践,必用)
> 临界区内:只放「必须原子执行的共享变量操作」,无任何多余代码
> 临界区外所有非核心逻辑计算、打印、sleep、遍历筛选、数据处理全部移出
✅ 正确示范(精简核心代码,最推荐)
```
# 写线程:临界区外准备数据 → 临界区内原子操作 → 临界区外打印
new_data = [MsgItem(id=2, content="测试")] # 临界区外准备
with mode_lock:
MSG_POOL.clear()
MSG_POOL.extend(new_data) # 仅保留核心操作
print("写入完成") # 临界区外打印
# 读线程:临界区内拷贝快照 → 临界区外筛选/遍历/读取
with mode_lock:
pool_snap = MSG_POOL.copy() # 仅拷贝,无其他操作
# 临界区外执行【全量读取】或【按条件读取】
target = [item for item in pool_snap if item.id == 2]
```
❌ 错误示范(绝对禁止)
```
with mode_lock:
MSG_POOL.clear()
MSG_POOL.extend(new_data)
print("写入完成") # 耗时的打印放临界区,锁持有时间变长
time.sleep(1) # 致命sleep放临界区读线程会被阻塞1秒
```
### ✅ 五、读线程 灵活读取策略
读线程的核心需求:**持续运行不阻塞、按需读取(全量 / 条件筛选)**,所有读取逻辑均遵循「**快照读取 + 临界区外处理**」,兼顾安全与效率,两种核心读取方式均无阻塞风险:
```
with mode_lock:
pool_snap = MSG_POOL.copy()
# 临界区外遍历全量数据
for msg in pool_snap:
print(f"id={msg.id}, content={msg.content}")
# 临界区外筛选指定内容,支持任意条件扩展
target_msg = None
for msg in pool_snap:
if msg.id == 2:
target_msg = msg
break
```
> 扩展如需读取其他条件id>5 / 内容包含指定字符),仅需修改临界区外的筛选逻辑即可,无需改动临界区。
### ✅ 六、优雅退出机制
所有线程均通过**全局布尔变量 `stop_flag`** 控制循环退出,无强制终止线程的风险:
```
stop_flag = False # 全局退出标志
# 写线程/读线程的循环条件
while not stop_flag:
# 核心业务逻辑
pass
# 主线程控制退出
time.sleep(运行时长)
stop_flag = True
# 等待线程执行完毕
写线程.join()
读线程.join()
```
## 延伸补充
### ✅ 1. 本次场景的「常见避坑点」
1. 不要为 MSG_POOL/MODE/ARGS 分别加锁 → 极易死锁,无任何必要;
2. MSG_POOL 的「清空 + 写入」必须在同一个临界区 → 最核心的坑,会导致读空池;
3. 不要在临界区内放 print/sleep/ 复杂计算 → 唯一的效率损耗来源;
4. 读复杂类型(字典 / 列表)时,必须用 `.copy()` 拷贝快照 → 避免临界区外写线程修改导致数据不一致。
### ✅ 2. 知识点浓缩(背诵级,解决「反复查资料」的核心需求)
1. 单写多读,一把锁足矣;
2. 临界区最小化,只留核心操作;
3. 清空 + 写入,必须原子化;
4. 复杂变量,拷贝快照读;
5. 安全优先,效率次之;
6. 标志位退出,线程更安全。
### ✅ 3. 进阶优化方向(按需使用,当前场景暂不需要)
所有优化均为「锦上添花」,当前场景的代码已经足够高效,只有当**读写频率极高(每秒万次)/MSG_POOL 数据量极大**时,才需要考虑,优先级从低到高:
1. 大列表优化:读指定 id 时,可在临界区内直接筛选,避免全量拷贝 → `target = next(m for m in MSG_POOL if m.id==2, None)`
2. 轻量锁替换:用 `threading.RLock()`(可重入锁)替代 `Lock()`,底层开销略低,用法完全一致;
3. 读写锁升级:若后续出现「多线程读,单线程写」,用 `threading.ReadWriteLock`,读线程之间不互斥,读效率提升 10 倍以上。