[python] 线程间同步之Lock RLock

为什么需要同步

同样举之前的例子,两个线程分别对同一个全局变量进行加减,得不到预期结果,代码如下:

total = 0
def add():
    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)

原因就是因为 +=-=并不是原子操作。可以使用dis模块查看字节码:

import dis
def add(total):
    total += 1
def desc(total):
    total -= 1
total = 0
print(dis.dis(add))
print(dis.dis(desc))

# 运行结果如下:
#   3           0 LOAD_FAST                0 (total)
#               3 LOAD_CONST               1 (1)
#               6 INPLACE_ADD
#               7 STORE_FAST               0 (total)
#              10 LOAD_CONST               0 (None)
#              13 RETURN_VALUE
# None
#   5           0 LOAD_FAST                0 (total)
#               3 LOAD_CONST               1 (1)
#               6 INPLACE_SUBTRACT
#               7 STORE_FAST               0 (total)
#              10 LOAD_CONST               0 (None)
#              13 RETURN_VALUE
# None

可以看到 add()函数虽然其中只有一行代码,但是字节码主要分为四个步骤:

  1. load 变量total
  2. load 常量 1
  3. 执行加法操作
  4. 对total进行赋值

同理,desc()函数的步骤相同,只是第三步改为执行减法。

假设一种极端情况,开始total = 0,首先线程1 load 变量total,得到值为0,切换到线程2,同样的到total为0,再次切换线程1 load常量1,执行加法,给total赋值得到1;然后线程2也 laod常量1,执行减法,给total赋值为-1。最终total为-1,而不是预期的0

期望中,必须在+=操作结束后,才能执行-=,所以线程同步的需求就出来了。

互斥锁Lock

threading模块中提供了threading.Lock类(互斥锁),基本用法如下:

import threading
lock = threading.Lock()
lock.acquire() # 获取锁
# dosomething…… # 临界区的代码只能被同时只能被一个线程运行
lock.release() # 释放锁

将上面的代码修改,即可得到正确结果:

import threading
total = 0
lock = threading.Lock()
def add(lock):
    global total
    for i in range(1000000):
        lock.acquire()
        total += 1
        lock.release()
def desc(lock):
    global total
    for i in range(1000000):
        lock.acquire()
        total -= 1
        lock.release()

thread1 = threading.Thread(target=add, args=(lock,))
thread2 = threading.Thread(target=desc, args=(lock,))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)

# 执行结果 0

可以看到,添加互斥锁之后,程序执行结果是正确的,但是用了互斥锁之后,同样有一些缺陷:

  • 添加锁之后,会影响程序性能。
  • 可能引起"死锁"。

死锁主要在两种情况下发生:

  1. 迭代死锁

    一个线程“迭代”请求同一个资源 ,会造成死锁。

    lock = threading.Lock()
    lock.acquire()
    lock.acquire()
    total += 1
    lock.release()
    lock.release()
    

    上例种,第一次请求资源后还未 release ,再次acquire,最终无法释放,造成死锁 。(可通过可重入锁解决这个问题)。

  2. 互相调用死锁

    两个线程中都会调用相同的资源,互相等待对方结束的情况 。假设A线程需要资源a,b,B线程也需要资源a,b,而线程A先获取资源a后,再获取资源b, B线程先获取资源b,再获取资源a。在A线程获取资源a,B线程获取资源b后,A线程在等待B线程释放资源b,而B线程在等待A线程释放资源a,从而死锁就发生了

    import threading
    import time
    lock_a = threading.Lock()
    lock_b = threading.Lock()
    
    def func1():
        global lock_a
        global lock_b
    
        lock_a.acquire()
        time.sleep(1)
        lock_b.acquire()
        time.sleep(1)
        lock_b.release()
        lock_a.release()
        
    def func2():
        global lock_a
        global lock_b
    
        lock_b.acquire()
        time.sleep(1)
        lock_a.acquire()
        time.sleep(1)
        lock_a.release()
        lock_b.release()
    
    thread1 = threading.Thread(target=func1)
    thread2 = threading.Thread(target=func2)
    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()
    print("program finished")
    # 程序会陷入死循环
    

    这个例子比较重要,开始理解错了,如果B线程获取了资源b,然后释放之后再获取资源a,这样是不会发生死锁的。只有在B线程获取了资源b,还没有释放的时候,获取了资源a,才会发生死锁。

可重入锁RLock

为解决同一线程种不能多次请求同一资源的问题,python提供了“可重入锁”:threading.RLockRLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源 。用法和threading.Lock类相同。

将上面迭代死锁的代码改写一下,就不会发生死锁,但注意,调用acquirerelease的次数必须相等。

lock = threading.RLock()
lock.acquire()
lock.acquire()
total += 1
lock.release()
lock.release()

一般不会写这么无聊的代码,但是有一种情况是可能发生的,在加锁区域调用了某个函数,而这个函数内部又申请了同样的资源。

lock = threading.RLock()
def dosomething(lock):
    lock.acquire()
    # do something
    lock.release()
    
lock.acquire()
dosomething(lock)
lock.release()

总结

  • 线程间访问同一变量需要同步。
  • 线程间加锁会导致性能损失。
  • 加锁可能产生死锁,迭代死锁互相调用死锁
  • 可重入锁可以避免迭代死锁。

参考

  1. 举例讲解 Python 中的死锁、可重入锁和互斥锁
  2. Python3高级编程和异步IO并发编程

你可能感兴趣的:([python] 线程间同步之Lock RLock)