python多线程需要同步么?

因为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是是释放,
然后它俩只能等待下一次重启了。

死锁的症状:

  1. 没人干活了,不占cpu,程序卡死
  2. 进程没有退出,占着内存资源

当程序卡死时,可以看看是否还用cpu、如果用说不定过一会就不卡了,如果不用可能是死锁了。

上一篇:Python-32-多线程2


下一篇:线程锁