在本项目中,读写锁主要应用于多线程服务器场景下的日志文件的读写,以及缓存的获取和更新。 多线程编程的准标准库posix pthread库拥有rwlock, 而python2.7自带的threading库没有读写锁,只有可重入锁RLock, 因此有必要自己实现一个读写锁以提升程序的并发性。
需要了解的概念
- 可重入锁。 可重入锁是指同一个锁可以多次被同一线程加锁而不会死锁。 实现可重入锁的目的是防止递归函数内的加锁行为,或者某些场景内无法获取锁A是否已经被加锁,这时如果不使用可重入锁就会对同一锁多次重复加锁,导致立即死锁。
- 读写锁。 读写锁与一般锁最大的区别是对同一共享资源多个线程的读取行为是并行的,同时保持该资源同一时刻只能由一个写进程独占,且写请求相对读请求有更高的优先级以防止writer starvation。( 一般锁同一时刻只能由一个线程独占,不论是读进程还是写进程, 即读写都是串行的,而读写锁读是并行的,写是串行的。)
读写锁的特点是:
2.1 当且仅当 锁没有被写进程占用且没有写请求时,可以获得读权限锁
2.2 当且仅当 锁没有被占用且没有读写请求时,可以获得写权限锁
读写锁的状态自动机可以参考下图
所有数据库都拥有读写锁,当必要时,会自动将读锁提升为写锁,称为lock promotion。
使用读写锁的注意事项
- 慎用promote ! 读写锁一般都有提权函数promote()用于将一个已经获取读锁的线程进一步提权获得写锁,这样做很容易导致程序死锁。例如,两个均已经获取读锁的线程A和B同时调用promote函数尝试获得写权限,线程A发现存在读线程B,需要等待B完成以获取写锁,线程B发现存在读线程A,需要等待线程A完成以获取写锁,循环等待发生,程序死锁。因此,当且仅当你能确定当前仅有一个读线程占有锁时才能调用promote函数。一个已经获取读锁的线程提权最好的办法是先释放读锁,然后重新申请写锁。
- 使用多个锁时保证加解锁顺序相反。 考虑以下错误代码:
A.lock();
B.lock();
Foo();
A.unlock();
Bar();
B.unlock();
如果在Bar函数中尝试重新获取锁A,那么获取B锁之前先要获取A锁的语义就被破坏了,因为你尝试在拥有锁B的情况下获取锁A,而不是意图实现的相反情况,并且Bar函数在A锁的关键区之外,该实现有可能导致死锁或其它未定义的情况。
正确的实现应该是按照C++中的RAII原则加解锁, 在python中使用with语法
lockA=threading.lock()
lockB=threading.lock()
with lockA:
with lockB:
Foo();
Bar()
读写锁目前的非官方实现
下列为目前发现的python rwlock的非官方实现
1. https://majid.info/blog/a-reader-writer-lock-for-python/
2. https://hdknr.github.io/docs/django/modules/django/utils/synch.html#RWLock
3. https://code.activestate.com/recipes/577803-reader-writer-lock-with-priority-for-writers/
4. https://github.com/azraelxyz/rwlock/blob/master/rwlock/rwlock.py
存在的问题
由于4个实现全部贴出代码内容较长,因此这里略去。推荐阅读[1]和[4]的实现。
1. [1]. 使用条件变量实现, [2]. 使用信号量实现,实际效果没有区别(信号量类有内部计数器,既可以当锁又可以当条件变量),但在当前需求下使用条件变量的版本更通俗易懂且[2]. 没有测试代码。 [3]. 中测试代码最全且使用了unittest,但自己实现的信号量_LightSwitch的auquire和release语义和python threading库正好相反,不推荐。 [4]. 的实现最规范也最复杂,已经提交给了issue8800, 与其它3个实现的主要区别是自己实现了可重入锁, 但是没有promote和demote接口也没有测试代码。
2. 除了[2]和[4],其它两个个版本的锁都是不可重入的。
通过分析4个版本的源码可以看出,4个版本[1]的实现最均衡,唯一实现了promote和de