186 lines
7.2 KiB
Markdown
186 lines
7.2 KiB
Markdown
#领域/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 倍以上。
|
||
|
||
|
||
|