Python 锁实现 (使用 threading 模块)
这可能是个基础的问题,但我刚接触Python的多线程编程,还不太确定正确的做法是什么。
我应该创建一个全局的锁对象(要么是全局的,要么是传来传去的),在需要上锁的地方都用这个锁吗?还是说我应该在每个需要用到锁的类里创建多个锁实例?看这两个简单的代码示例,哪种做法更好呢?主要的区别在于,第二个示例中在类A和类B中使用的是同一个锁实例,而第一个示例中则使用了多个锁实例。
示例 1
class A():
def __init__(self, theList):
self.theList = theList
self.lock = threading.Lock()
def poll(self):
while True:
# do some stuff that eventually needs to work with theList
self.lock.acquire()
try:
self.theList.append(something)
finally:
self.lock.release()
class B(threading.Thread):
def __init__(self,theList):
self.theList = theList
self.lock = threading.Lock()
self.start()
def run(self):
while True:
# do some stuff that eventually needs to work with theList
self.lock.acquire()
try:
self.theList.remove(something)
finally:
self.lock.release()
if __name__ == "__main__":
aList = []
for x in range(10):
B(aList)
A(aList).poll()
示例 2
class A():
def __init__(self, theList,lock):
self.theList = theList
self.lock = lock
def poll(self):
while True:
# do some stuff that eventually needs to work with theList
self.lock.acquire()
try:
self.theList.append(something)
finally:
self.lock.release()
class B(threading.Thread):
def __init__(self,theList,lock):
self.theList = theList
self.lock = lock
self.start()
def run(self):
while True:
# do some stuff that eventually needs to work with theList
self.lock.acquire()
try:
self.theList.remove(something)
finally:
self.lock.release()
if __name__ == "__main__":
lock = threading.Lock()
aList = []
for x in range(10):
B(aList,lock)
A(aList,lock).poll()
2 个回答
一般来说,使用一个全局锁效率较低(因为竞争多),但安全性更高(没有死锁的风险),前提是这个锁是RLock
(可重入锁),而不是普通的Lock
。
潜在的问题出现在一个线程在持有锁的情况下,尝试获取另一个(或同一个)锁,比如调用了另一个包含acquire
的函数。如果一个已经持有锁的线程再去获取这个锁,如果是普通的Lock
,它会一直阻塞下去;但如果是稍微复杂一点的RLock
,就可以顺利进行——这就是为什么它叫做可重入,因为持有这个锁的线程可以再次“进入”(获取锁)。简单来说,RLock
会记录是哪个线程持有它,以及这个线程获取锁的次数,而普通的Lock
则不会记录这些信息。
当有多个锁的时候,死锁问题就出现了,比如一个线程先尝试获取锁A,然后获取锁B,而另一个线程先获取锁B,再获取锁A。如果发生这种情况,最终你会发现第一个线程持有锁A,第二个线程持有锁B,而它们各自都在尝试获取对方持有的锁——这样就会导致两个线程都永远阻塞。
防止多个锁死锁的一种方法是确保无论哪个线程在获取锁时,锁的获取顺序都是一致的。然而,当每个实例都有自己的锁时,这样的组织方式就变得非常复杂和困难。
如果你在每个类里使用不同的锁对象,就有可能出现死锁的情况。比如说,一个操作先锁住了A,然后再锁住B,而另一个操作则是先锁住B,再锁住A,这样就会造成互相等待,谁也动不了。
如果你只用一个锁,那就意味着你的代码只能单线程执行,而有些操作其实可以并行进行。虽然在Python中,这个问题没有那么严重,因为Python本身就有一个全局锁,但如果你在写文件的时候持有全局锁,Python会释放这个全局锁,但其他所有的操作都会被阻塞。
所以这其实是一个权衡的问题。我建议使用小锁,这样可以最大化并行执行的机会,但要注意一次不要同时申请多个锁,也尽量不要长时间持有一个锁,除非真的必要。
就你提到的具体例子来说,第一个例子是有问题的。如果你对theList
进行锁定操作,那么每次都必须使用同一个锁,否则就没有真正锁住任何东西。虽然在这里,list.append和list.remove操作本身是原子性的,不会有问题,但如果你确实需要锁定对列表的访问,就一定要确保每次都用同一个锁。最好的办法是把列表和锁作为一个类的属性,然后强制所有对列表的访问都通过这个类的方法来进行。这样,你就只需要传递这个包含类的对象,而不是列表或锁。