关于 Python 多线程的一个疑问,见笑!
既然 Python 的多线程模式是伪多线程,即同一时刻只能有一个线程执行。那么为什么还会有 threading.Queue 这种所谓线程安全的变量类型呢?我理解应该任何数据类型( list、tuple 等)都是线程安全的。
有谁可以给解释一下吗?
关于 Python 多线程的一个疑问,见笑!
i += 1
帖子回复:
你这个问题问得很实际,很多刚接触Python多线程的朋友都会卡在这里。核心原因在于:Python的全局解释器锁(GIL)。
简单说,GIL让同一时刻只有一个线程能执行Python字节码。所以,即使你开了多个线程,它们在CPU密集型计算上也是“排队”执行的,无法真正利用多核并行。你的count += 1看起来简单,但涉及读取、计算、写入三步,线程切换就会导致更新丢失。
直接上代码,用threading.Lock解决:
import threading
def worker(lock):
global count
for _ in range(100000):
with lock: # 获取锁
count += 1
if __name__ == "__main__":
count = 0
lock = threading.Lock()
threads = []
for _ in range(10):
t = threading.Thread(target=worker, args=(lock,))
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"Expected: 1000000, Got: {count}") # 现在每次都是 1000000
这里with lock:保证了同一时间只有一个线程能执行count += 1,避免了冲突。当然,加锁会降低并发效率,但对于这种共享资源的写操作是必须的。
如果真是CPU密集型任务,建议用multiprocessing模块绕过GIL。如果是I/O密集型(比如网络请求、文件读写),用多线程依然很合适,因为线程在等待I/O时会释放GIL,其他线程可以继续运行。
总结:加锁就对了。
感谢。通过一通试验之后,我貌似明白了。
原子操作吗? 看了前两楼还是没太懂
同一时刻只能有一个线程执行指的是 byte code 级别的执行,一个包含多个 byte code 的函数中仍有可能会发生线程调度的
就是原子性问题呗 虽然有 gil 但你想象一个场景 比如你对 list 做 append append 要做好几个操作 某个线程做了一部分的操作 然后切到另一个线程再做 append 这会有啥后果
cpu 支持并发,达到某些条件会线程切换,Python 因为 GIL 而不支持真正的并行,GIL 同一时刻( cpu 时间片)只允许一个线程,不是在一段时间内只允许一个线程执行。所以线程不安全的变量,在多线程环境中,可能在变量并未发生变化时,就被切换到另外一个线程中被使用了。https://www.zhihu.com/question/23030421
真巧,针对这个场景,我刚刚用代码跑了一下 [启动 4 个线程,一个全局的 list 变量,每个线程都对这个 list 进行 append 操作 100000 次] 。跑完之后,对于这个 list 的长度没什么影响,里边的元素值也没什么影响,只是对元素的排列顺序有影响。
把这个实验中的 list 改为一个 int 值,每个线程对这个值进行+1 操作 100000 次,最终查看结果,发现值对不上。这就是 1 楼 i += 1 的意思。
这样吗(翻车了……
回头看看 list 底层有什么魔法
python 官方文档提到了在 cpython 的实现中 list 的 append 是原子的
https://docs.python.org/3.7/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe
其实一个操作是不是原子的有两种评判标准:
1、对于纯 Python 代码,是不是只有一条 byte code
2、对于 C 实现的函数,内部有没有释放 GIL
不是 CPU 密集运算干嘛用多进程? I/O 操作的单核线程并发相比进程开销要高效的多
你用过 python 吗?知道什么时候用线程什么时候用进程吗?不知道就别乱说
还有,那些喜欢拿 python 的 GIL 说事情的人麻烦好好看看自己的代码有没有资格被 GIL 影响
多进程来传递个 FD 看能不能把你搞疯掉
感觉 python 的 multprocessing 确实有黑科技,试过把主 loop 用 prange 重写只快了百分之 30
原来如此 感谢
因为多线程之间会进行上下文切换,执行非原子操作的时候可能在过程中挂起当前线程,之后继续执行的的时数据可能已不是最新了
感谢。
#14 我没用过,谢谢
#15 传 fd 本身就是糟糕的设计,即便是线程也要尽量避免。
在保证一个 fd 只会被一个线程操作的情况下,传递 fd 没有任何糟糕的,如果你觉得糟糕,那只能说明你程序写的烂
#24 你说得对,我写的烂

