关于Python中GIL与collections.deque线程安全性的疑问及示例需求
关于Python中GIL与collections.deque线程安全性的疑问及示例需求
嘿,这个问题问得特别戳痛点,很多刚接触Python多线程的同学都会被GIL和线程安全的关系绕晕,我来给你掰扯清楚~
先搞懂GIL和线程安全的核心矛盾
GIL确实保证同一时刻只有一个线程在执行Python字节码,但线程切换可能发生在任意字节码执行完毕之后。很多看似“单一操作”的Python代码,比如列表的append(),其实背后是由多个字节码指令组成的。如果线程在这些字节码之间切换,就可能导致数据结构处于不一致的状态。
举个简单的逻辑例子,列表的append()大致会做这几步:
- 检查列表容量是否足够
- 不够的话扩容分配新内存
- 将元素添加到新位置
- 更新列表的长度
这几步对应多个字节码,要是线程A刚做完步骤3,还没更新长度,GIL就切换给了线程B,线程B去读列表长度的时候拿到的还是旧值,后续操作自然就会出问题。
为什么collections.deque是线程安全的?
deque的关键操作(比如append()、appendleft()、popleft()、pop())都是原子操作——也就是说,这些操作在底层被实现成了不可分割的单一步骤,不会被GIL的线程切换打断。哪怕多个线程同时调用这些方法,也不会出现数据混乱,因为每个操作要么完全执行完,要么完全没开始。
具体示例对比
1. 列表的非线程安全演示
我们用多个线程同时往列表里添加元素,看看结果:
import threading def add_to_list(lst, num): for _ in range(num): lst.append(1) my_list = [] threads = [] # 开5个线程,每个线程加1000个元素 for _ in range(5): t = threading.Thread(target=add_to_list, args=(my_list, 1000)) threads.append(t) t.start() for t in threads: t.join() print(f"预期长度:5000,实际长度:{len(my_list)}")
运行这个代码,你会发现实际长度经常小于5000,偶尔甚至会出现索引错误——这就是因为append()不是原子操作,线程切换导致部分添加操作被“打断”了。
2. 用deque解决线程安全问题
把上面的列表换成deque,再试试:
import threading from collections import deque def add_to_deque(dq, num): for _ in range(num): dq.append(1) my_deque = deque() threads = [] for _ in range(5): t = threading.Thread(target=add_to_deque, args=(my_deque, 1000)) threads.append(t) t.start() for t in threads: t.join() print(f"预期长度:5000,实际长度:{len(my_deque)}")
不管你运行多少次,实际长度都会是5000,完全符合预期。这就是因为deque.append()是原子操作,不会被线程切换打断,保证了数据的一致性。
备注:内容来源于stack exchange,提问作者Aniket Thakur
相关产品推荐
相关产品推荐

