🧠 什么是共享写操作?
假设有多个线程同时访问一个变量、列表、文件,并且都在 “修改”它,这就是“共享写操作”。
比如:
my_list = []def worker():for i in range(1000):my_list.append(i)
如果多个线程同时运行 worker()
,就会同时往 my_list
里加数据,结果就可能丢数据、重复数据,因为:
Python 在执行 append 的时候被另一个线程打断了,就会导致写错!
✅ 怎么加锁?
用 Python 的 threading.Lock()
,可以防止多个线程同时进入“关键区”。
🔒 正确的加锁方式如下:
import threadinglock = threading.Lock() # 创建一把锁my_list = []def worker():for i in range(1000):with lock: # 加锁my_list.append(i) # 关键操作
关键语法是:
with lock:# 这里是临界区(共享资源修改)
🔐 这段代码的含义是:
- 每次有线程要操作共享资源时,先**“申请钥匙”**
- 如果锁已经被别的线程拿了,就等着
- 拿到锁后,线程可以安全修改数据,改完会自动释放锁
这段代码:
with results_lock:results.append(item)
就代表:
- 多个线程都要往
results
这个共享列表中append
- 所以必须加锁,用
results_lock
包起来,防止冲突 - 不加锁可能会导致:部分数据没加进去、程序报错、数据错乱
🧪 真实例子演示(无锁 vs 有锁)
import threadingcount = 0
lock = threading.Lock()def add():global countfor _ in range(100000):with lock:count += 1threads = []
for _ in range(5):t = threading.Thread(target=add)t.start()threads.append(t)for t in threads:t.join()print("最终结果:", count)
如果不加锁,会发现 count
的结果可能会小于 500000!
我们再以一个“银行账户”的例子来模拟真实问题:
🧪 场景模拟:多个线程给一个银行账户转账,每次加 1 块钱,转 10 万次
我们用两个版本来比较:
✅ 有加锁的版本(正确做法)
import threadingbalance = 0 # 银行账户初始余额
lock = threading.Lock()def deposit():global balancefor _ in range(100000): # 每个线程转账 10 万次,每次 1 元with lock:balance += 1threads = []
for _ in range(5): # 启动 5 个线程一起转账t = threading.Thread(target=deposit)t.start()threads.append(t)for t in threads:t.join()print("✅加锁后最终余额:", balance) # 期望结果:500000
📌 输出示例:
✅加锁后最终余额: 500000
❌ 不加锁的版本(容易出问题)
import threadingbalance = 0 # 初始余额def deposit():global balancefor _ in range(100000):balance += 1 # ❌ 多线程同时写,容易数据冲突threads = []
for _ in range(5): # 启动 5 个线程t = threading.Thread(target=deposit)t.start()threads.append(t)for t in threads:t.join()print("❌未加锁最终余额:", balance) # 理想应该是 500000,实际可能是 47xxxx
📌 输出示例:
❌未加锁最终余额: 473861
🧠 为什么不加锁就会出错?
这是因为:
balance += 1
其实包含三个步骤:
- 从内存中读取
balance
- 计算
balance + 1
- 写回内存
多个线程同时执行时,可能都读到了旧值,然后计算后覆盖写入,导致有的“+1”被覆盖了!
📊 可视化比喻:
想象你和你朋友一起在一张纸上记录余额,你俩同时看到余额是 100:
- 你算完后写 101
- 朋友也算完后写 101
最后纸上的余额还是 101,本来应该是 102,这就是数据丢失!
✅ 最佳实践总结
场景 | 是否需要加锁 |
---|---|
多线程修改共享变量(如列表、字典、计数器) | ✅ 必须加锁 |
多线程只读取共享变量 | ❌ 不需要加锁 |
多线程读写文件 | ✅ 加锁写操作 |
每个线程操作独立变量 | ❌ 不需要加锁 |
✅ 总结一句话:
凡是多线程中涉及“写操作”的变量、文件、列表,一定要加锁保护,避免多个线程抢着改。
只读不写的就不用加锁,比如读取文件、只看数据等。