如何编写可靠示例演示多线程中Lock解决的线程不安全问题?
原代码没出现交错的原因
你的代码里用了list.append(),但在CPython中这个操作是原子性的(受全局解释器锁GIL保护),单个append不会被其他线程打断。再加上线程调度的特性,某个线程可能连续执行完几百次append才会被切换,所以最终输出是连续的同值元素,不会出现预期的交错。
要复现线程不安全,得构造非原子的共享数据操作——也就是把一个操作拆成多个步骤,中间可以被其他线程打断。
修改后的无锁示例(可稳定复现问题)
把append替换成"先获取列表长度,再通过索引赋值"的逻辑,这个过程是分步的,中间会被线程切换打断,导致数据覆盖或丢失:
import threading shared_list = [] def simulate_unsafe_append(value): for _ in range(1000): # 非原子操作:先读长度,再赋值 current_len = len(shared_list) # 可选:加微小延迟放大线程切换概率,更容易复现问题 # threading.sleep(0.0001) shared_list.insert(current_len, value) # 手动模拟append逻辑 threads = [] for i in range(10): thread = threading.Thread(target=simulate_unsafe_append, args=(i,)) threads.append(thread) thread.start() for thread in threads: thread.join() print(f"实际列表长度: {len(shared_list)}") print(f"预期列表长度: {10 * 1000}") # 查看前20个元素,会发现大量重复值或缺失值 print(shared_list[:20])
运行这段代码,你会发现实际列表长度远小于预期的10000,而且列表里会出现大量重复元素——这就是线程不安全导致的:多个线程同时读取到相同的current_len,然后往同一个位置插入元素,后面的插入会覆盖前面的,最终造成元素丢失。
如果觉得问题不够明显,可以把注释里的threading.sleep(0.0001)打开,强制让线程在两步之间切换,问题会更突出。
加上Lock的正确示例
现在给共享操作加上threading.Lock(),保证同一时间只有一个线程执行非原子操作:
import threading shared_list = [] lock = threading.Lock() # 创建锁对象 def safe_append(value): for _ in range(1000): with lock: # 使用with自动获取/释放锁,避免手动操作遗漏 current_len = len(shared_list) shared_list.insert(current_len, value) threads = [] for i in range(10): thread = threading.Thread(target=safe_append, args=(i,)) threads.append(thread) thread.start() for thread in threads: thread.join() print(f"实际列表长度: {len(shared_list)}") print(f"预期列表长度: {10 * 1000}") print(shared_list[:20])
加上锁之后,每次只有一个线程能执行current_len = len(shared_list)和shared_list.insert(...)这两步,不会出现多个线程同时操作的情况,最终列表长度刚好是10000,元素也会出现交错(因为线程会在锁释放后切换)。
补充说明
如果你还是想保留append的形式来演示,可以把append和其他操作结合,比如先读取列表最后一个元素再append,这样整个过程是非原子的:
import threading shared_list = [] def unsafe_operation(value): for _ in range(1000): # 非原子操作:读最后一个元素 + append if shared_list: last = shared_list[-1] shared_list.append(value) # 后续线程创建和join逻辑和之前一致
不过这种方式的问题不如手动实现append明显,还是推荐用前面的"读长度+插入"的方式,能稳定复现数据丢失的问题,更直观说明Lock的必要性。
内容的提问来源于stack exchange,提问作者memelord23

