当使用多个线程来访问同一个数据时,很容易“偶然”出现线程安全问题。为了解决这个问题,实现线程安全的控制,Python 的 threading 模块引入了锁(Lock)。Lock 是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程在开始访问共享资源之前应先请求获得 Lock 对象。当对共享资源访问完成后,程序释放对 Lock 对象的锁定。

threading 模块提供了 Lock 和 RLock 两个类,它们都提供了如下两个方法来加锁和释放锁:

  1. acquire(blocking=True, timeout=-1):请求对 Lock 或 RLock 加锁,其中 timeout 参数指定加锁多少秒。
  2. release():释放锁。

Lock 和 RLock 的区别如下:
threading.Lock:它是一个基本的锁对象,每次只能锁定一次,其余的锁请求,需等待锁释放后才能获取。
threading.RLock:它代表可重入锁(Reentrant Lock)。对于可重入锁,在同一个线程中可以对它进行多次锁定,也可以多次释放。如果使用 RLock,那么 acquire() 和 release() 方法必须成对出现。如果调用了 n 次 acquire() 加锁,则必须调用 n 次 release() 才能释放锁。

由此可见,RLock 锁具有可重入性。也就是说,同一个线程可以对已被加锁的 RLock 锁再次加锁,RLock 对象会维持一个计数器来追踪 acquire() 方法的嵌套调用,线程在每次调用 acquire() 加锁后,都必须显式调用 release() 方法来释放锁。所以,一段被锁保护的方法可以调用另一个被相同锁保护的方法。

在实现线程安全的控制中,比较常用的是 RLock。通常使用 RLock 的代码格式如下:

class X:
    #定义需要保证线程安全的方法
    def m () :
        #加锁
        self.lock.acquire()
        try :
            #需要保证线程安全的代码
            #...方法体
        #使用finally 块来保证释放锁
        finally :
            #修改完成,释放锁
            self.lock.release()

使用 RLock 对象来控制线程安全,当加锁和释放锁出现在不同的作用范围内时,通常建议使用 finally 块来确保在必要时释放锁。

通过使用 Lock 对象可以非常方便地实现线程安全的类,线程安全的类具有如下特征:

  • 该类的对象可以被多个线程安全地访问。
  • 每个线程在调用该对象的任意方法之后,都将得到正确的结果。
  • 每个线程在调用该对象的任意方法之后,该对象都依然保持合理的状态。

这意味着,并发线程在任意时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区),所以在同一时刻最多只有一个线程处于临界区内,从而保证了线程安全。

可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略:

  • 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。例如,上面 Account 类中的 account_no 实例变量就无须同步,所以程序只对 draw() 方法进行了同步控制。
  • 如果可变类有两种运行环境,单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中使用钱程不安全版本以保证性能,在多线程环境中使用线程安全版本。