因为GIL的存在,每次只能执行一个线程,那Python还存在变量同步的问题么?
声明一个变量,起两个线程各对这个变量加100,0000次,观察结果是否为200,0000
预期:
如果不为200,0000,那说明Python的变量也需要同步。
代码:
import threading
import time
count = 0
def f(name):
global count
for i in range(1000000):
count = count + 1
print(f"thread {name} end")
threading.Thread(target=f, args=('t1',)).start()
threading.Thread(target=f, args=('t2',)).start()
time.sleep(1)
print(f"sleep end, count is {count}")
输出:
thread t1 end
thread t2 end
sleep end, count is 1465522
Python虽然没有发挥出多核CPU的优势,却把线程不安全的问题带来了,它在执行时也会编译成
字节码,可以看下上面的代码翻译成什么了:
import dis
count = 0
def f(name):
global count
for i in range(1000000):
count = count + 1
print(dis.dis(f))
输出:
9 0 SETUP_LOOP 24 (to 26)
2 LOAD_GLOBAL 0 (range)
4 LOAD_CONST 1 (1000000)
6 CALL_FUNCTION 1
8 GET_ITER
>> 10 FOR_ITER 12 (to 24) # 跳转到迭代运算
12 STORE_FAST 1 (i)
10 14 LOAD_GLOBAL 1 (count) # 读取全局变量 count
16 LOAD_CONST 2 (1) # 读取常量 1
18 BINARY_ADD # 加运算
20 STORE_GLOBAL 1 (count) # 结果 count=1 回写到count
22 JUMP_ABSOLUTE 10
>> 24 POP_BLOCK
>> 26 LOAD_CONST 0 (None)
28 RETURN_VALUE
None
想象一下两个线程都执行这个方法,第一个执行到指令16或者18的时候第二个线程执行指令14
也就是他们进行加操作时读取的是同一个count,比如都是8,他们的计算结果都是9,也就少加了一次。
运算的次数越多,出现上面的情况也就越多。
解决版本,把读取count和+1操作合并成一个原子操作通过互斥锁:
import threading
from threading import Lock
import time
count = 0
lock = Lock()
def f(name):
global count
global lock
for i in range(1000000):
lock.acquire() # 获取锁
count = count + 1
lock.release() # 释放锁
print(f"thread {name} end")
threading.Thread(target=f, args=('t1',)).start()
threading.Thread(target=f, args=('t2',)).start()
time.sleep(1)
print(f"sleep end, count is {count}")
这样执行的结果就正常了,来再看下指令:
from threading import Lock
import dis
lock = Lock()
count = 0
def f(name):
global count
global lock
for i in range(1000000):
lock.acquire()
count = count + 1
lock.release()
print(dis.dis(f))
输出:
/Users/wuhf/anaconda3/envs/cookdata/bin/python3 /Users/wuhf/PycharmProjects/cookdata/cookdata/tests/run_lock.py
10 0 SETUP_LOOP 40 (to 42)
2 LOAD_GLOBAL 0 (range)
4 LOAD_CONST 1 (1000000)
6 CALL_FUNCTION 1
8 GET_ITER
>> 10 FOR_ITER 28 (to 40)
12 STORE_FAST 1 (i)
11 14 LOAD_GLOBAL 1 (lock)
16 LOAD_METHOD 2 (acquire)
18 CALL_METHOD 0
20 POP_TOP
12 22 LOAD_GLOBAL 3 (count)
24 LOAD_CONST 2 (1)
26 BINARY_ADD
28 STORE_GLOBAL 3 (count)
13 30 LOAD_GLOBAL 1 (lock)
32 LOAD_METHOD 4 (release)
34 CALL_METHOD 0
36 POP_TOP
38 JUMP_ABSOLUTE 10
>> 40 POP_BLOCK
>> 42 LOAD_CONST 0 (None)
44 RETURN_VALUE
None
Process finished with exit code 0
关键指令分析:
执行到指令10进行循环迭代,进入循环体执行执行指令11,它加载并获取锁,接着指令12被合并成
一个大指令STORE_FAST
,这里面读取count,加操作并且回写,指令13释放锁。
Lock虽然解决同步的问题,但是带来的潜在的问题:死锁!如果两端段代码,两个线程粉笔执行,
每一段都需要两把锁,并且都获取了对方的锁那会怎样?
from threading import Lock
lock_a = Lock()
lock_b = Lock()
def f1():
global lock_a
global lock_b
lock_a.acquire()
lock_b.acquire()
print("Ha ha")
lock_a.release()
lock_b.release()
def f2():
global lock_a
global lock_b
lock_b.acquire()
lock_a.acquire()
print("He he")
lock_b.release()
lock_a.release()
想象一下,线程1执行f1,线程2执行f2,线程1执行刚执行完lock_a.acquire()
线程2也刚执行完
lock_b.acquire()
,这时候它俩手里各有一把锁,并且还需要一把锁,线程1要执行lock_b.acquire()
,
但是这个已经被线程2持有了,要等待线程2释放,线程2执行到lock_a.acquire()
等待线程1是是释放,
然后它俩只能等待下一次重启了。
死锁的症状:
- 没人干活了,不占cpu,程序卡死
- 进程没有退出,占着内存资源
当程序卡死时,可以看看是否还用cpu、如果用说不定过一会就不卡了,如果不用可能是死锁了。