zoukankan      html  css  js  c++  java
  • 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、如果用说不定过一会就不卡了,如果不用可能是死锁了。

  • 相关阅读:
    Android 常见工具类封装
    Android Logcat 封装类
    Android 四大组件之 " Activity "
    "浅谈Android"第一篇:Android系统简介
    罗列的书单
    关于多层架构一些思考
    LeetCode 330. Patching Array
    LeetCode 315. Count of Smaller Numbers After Self(线段树,树状数组)
    LeetCode 316. Remove Duplicate Letters(贪心)
    LeetCode 327. Count of Range Sum(线段树)
  • 原文地址:https://www.cnblogs.com/oaks/p/13376433.html
Copyright © 2011-2022 走看看