【Python】多线程之可重入锁

825 阅读3分钟

「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。

在上一篇文章 《【Python】线程间同步的实现与代码》中,已经介绍了线程间同步常用的方法,及如何在 Python 中使用互斥锁 Lock,在 threading 中,除了 Lock,还有另一种锁 RLock

可重入锁

广义上的可重入锁指的是可重复可递归调用的锁。

也就是在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。

LockRLock

threading 中,有 LockRLock 两种锁。

Lock 被称为原始锁

  • 有两个基本方法, acquire() 和 release() 
    • 当状态为非锁定时, acquire() 将状态改为锁定并立即返回
    • 当状态是锁定时, acquire() 将阻塞至其他线程调用 release() 将其改为非锁定状态
    • release() 只在锁定状态下调用,将状态改为非锁定并立即返回
    • 如果尝试释放一个非锁定的锁,则会引发 RuntimeError  异常
  • 在锁定时不属于特定线程,可以在一个线程中上锁,在另一个线程中解锁
  • 支持上下文管理协议,即支持 with 语句

RLock 被称为重入锁

  • 有两个基本方法, acquire() 和 release() 
    • 若要锁定锁,线程调用其 acquire() 方法,一旦线程拥有了锁,方法将返回
    • 若要解锁,线程调用 release() 方法
    • 线程必须在每次获取锁时释放一次
  • acquire()/release() 对可以嵌套,重入锁必须由获取它的线程释放
  • 一旦线程获得了重入锁,同一个线程再次获取它将不阻塞
  • 支持上下文管理协议,即支持 with 语句

重入原始锁

threading 中的 Lock 是原始锁,线程在获取到锁后,如果试图再次获取,则会被阻塞。

举个栗子:

import threading

mutex = threading.Lock()

def do_something():
    if mutex.acquire():         # 获取锁
        print(threading.currentThread().name + " get lock")
        if mutex.acquire():     # 再次获取锁
            print(threading.currentThread().name + " get lock against")
            mutex.release()
        mutex.release()
        

t1 = threading.Thread(target= do_something, name= "t1")
t2 = threading.Thread(target= do_something, name= "t2")

t1.start()
t2.start()

t1.join()
t2.join()

代码输出为: t1 get lock

上述代码中,线程 t1 获取锁后,试图再次获取锁,此时锁无法被再次获取,线程被阻塞。由于线程 t1 获取了锁,线程 t2 无法获取锁,处于阻塞状态。

重入可重入锁

下面,将上述代码中锁改为可重入锁,既把 mutex = threading.Lock() 改为 mutex = threading.RLock()

运行代码,输出为:

get lock
t1 get lock against
t2 get lock
t2 get lock against

上述代码中,线程 t1 获取锁后,再次获取锁,此时锁可以被再次获取,线程得到锁后执行操作,完成操作后释放锁。线程 t1 释放锁后,线程 t2 获取锁,执行操作。

可重入锁应用场景

现在有一个方法,为了保证数据安全,其会申请锁:

def func():
    if lock.acquire():
        print(threading.currentThread().name + " do something")
        lock.release()

现在有另一个方法,为了保证数据安全,同样会申请锁,并且会调用上面的方法:

def func2():
    if lock.acquire():
        print(threading.currentThread().name + " do something")
        
        print(threading.currentThread().name + " call func")
        func()
        
        lock.release()

此时,变量 lock 应该是一个可重入锁,否则线程调用 func() 后,将会被阻塞。