
total = 0 def add(): #1. dosomething1 #2. io 操作 # 1. dosomething3 global total for i in range(1000000): total += 1 def desc(): global total for i in range(1000000): total -= 1 import threading thread1 = threading.Thread(target=add) thread2 = threading.Thread(target=desc) thread1.start() thread2.start() thread1.join() thread2.join() print(total) # 178412 为啥不是 0. 已经做好被喷准备.
1 simapple 2020 年 5 月 22 日 加锁 |
2 chanchancl 2020 年 5 月 22 日 简单来说 total 不是线程安全的, 这个不只在 Python 中会出现,而是任何多线程语言下都会出现的现象, 解决方案就是访问前加锁, 或者用原子操作 不过我感觉你是来钓鱼的 |
3 chaleaoch OP @chanchancl 不是钓鱼 python 不是有 GIL 吗? |
5 sujin190 2020 年 5 月 22 日 你可以 dis 看下生成的字节指令就知道了,+= 1 也是需要好多条指令的,python 的 GIL 应该只是在单条字节指令保证原子性吧,但是你一行代码很多时候都是多条字节指令的吧 |
6 chenxytw 2020 年 5 月 22 日 via iPhone += 和 -= 不是原子的 |
7 xiaolinjia 2020 年 5 月 22 日 1. GIL 意思是任何时候只有一个线程运行 2. 因为增量赋值不是原子操作,具体可看 dis.dis('a += 1') 的字节码不止一步 3. 线程是系统调度的,你不知道何时切换 4. 好,那你怎么就知道在做增量赋值的字节码某一步的时候,不会切换到了另一个线程呢? |
8 lithbitren 2020 年 5 月 22 日 全局定义一个 lock = threading.Lock(),+=、-=之前增加一个 with lock:的块 |
9 jugelizi 2020 年 5 月 22 日 via iPhone 典型程序员思维。 难道一百个男生 一百个女生。 就一定是每个男生都有对象吗 |
10 vagrantear 2020 年 5 月 22 日 @jugelizi 程序员确实想每个男生都有对象(逃 |
11 dahuahua 2020 年 5 月 22 日 GIL 锁也不一定安全,任何指令运行都是有一个周期的,到时间还是乖乖把锁交出去了。 |
12 CzaOrz 2020 年 5 月 23 日 我猜你是这样想的... 无论先后,只要有 1000000 的+或者-执行,那么最后的结果肯定为 0, 比如先执行 999999 次+,执行 1 次-,再跑 1 次+,再跑 999999 次-, 无论过程如何,结果都应该为 0 才对 实际 pythonGIL 全局解释器,若无其他因素,应该是每个线程执行 100 字节,就会释放锁,执行其他线程 两个线程共同操作同一个全局变量,就会导致结果不可预测, 你已经没有办法模拟出内部的具体执行流程了,就像你的数据结果 178412 没有任何意义 多跑几次也会是不一样的结果 像楼上大佬们说的安全、加锁之类的,就是为了保证每次能够完整的执行一次+或者- |
13 yuruizhe 2020 年 5 月 23 日 个人猜测 1.初始 count=0 2.线程 add 取 count=0 3.线程 desc 取 count=0 4.线程 add 计算 tmp=count+1=0 5.线程 desc 计算 tmp=count-1=-1 6.线程 desc 写入 count=-1 7 线程 add 写入 count=1 ... 由于调度,某些写入操作的数值被覆盖掉了,没被下一次计算正确读取 |
14 wuwukai007 2020 年 5 月 23 日 += 不是线程安全的 |
15 black11black 2020 年 5 月 23 日 你可以 global list,然后在每个线程里 for ... list.append(1),最后 sum(list)大概就能得到你想要的结果了。狗头 |
16 chanchancl 2020 年 5 月 25 日 @chaleaoch GIL 保证同时只有一个线程运行,但是并不保证多线程之间的执行顺序。 比如 Thread1 Thread2 Lock GIL read total,0 add total,1 此时进入休眠 Unlock GIL Lock GIL read total, 0 add total, 1 write total, 1 休眠 Unlock GIL Lock GIL write total 此时 total 的值还是 1 Unlock GIL 总的来说,GIL 保护的是解释器级别的数据安全,比如对象的引用计数,垃圾分代数据等等,具体参考垃圾回收机制。而对于程序中自己定义的数据则没有任何的保护效果,所以当程序中出现了共享自定义的数据时就要自己加锁。 这句话来自 : https://www.cnblogs.com/liuxiaolu/p/10215629.html |
17 chanchancl 2020 年 5 月 25 日 完了,v2 把我的空格吞了, 总之上面的回复, Lock GIL read total,0 add total,1 此时进入休眠 Unlock GIL 这一部分,由线程一执行 Lock GIL read total, 0 add total, 1 write total, 1 休眠 Unlock GIL 这一部分,由线程二执行 Lock GIL write total 此时 total 的值还是 1 Unlock GIL 这一部分,由线程一执行 其实由线程几执行无所谓,重要的是这里有一个切换进程的动作。 Python 的解释器你可以理解为一个可以执行指令的 CPU 而赋值这些操作都不是原子的,不应该依赖 GIL 去做任何事, GIL 本身也不是 Python 的特性,而是 CPython 这个实现的特性 |
18 Fasion 2020 年 5 月 27 日 total += 1 不是一个原子操作,在 Python 虚拟机内部,由多条字节码构成,字节码是原子的: >>> dis.dis(compile('total += 1', '', 'exec')) 1 0 LOAD_NAME 0 (total) 2 LOAD_CONST 0 (1) 4 INPLACE_ADD 6 STORE_NAME 0 (total) 8 LOAD_CONST 1 (None) 10 RETURN_VALUE 线程在任意字节码间都可能发生切换,因此多线程下可能会发生数值相互覆盖的问题。 对 Python 虚拟机实现该兴趣的童鞋可以关注我写的专栏: https://www.imooc.com/read/76,里面有详细介绍。 |
19 Fasion 2020 年 5 月 27 日 专栏地址写得有点问题,更新一下: https://www.imooc.com/read/76 |