Python 锁实现 (使用 threading 模块)

2 投票
2 回答
8428 浏览
提问于 2025-04-15 22:23

这可能是个基础的问题,但我刚接触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 个回答

9

一般来说,使用一个全局锁效率较低(因为竞争多),但安全性更高(没有死锁的风险),前提是这个锁是RLock(可重入锁),而不是普通的Lock

潜在的问题出现在一个线程在持有锁的情况下,尝试获取另一个(或同一个)锁,比如调用了另一个包含acquire的函数。如果一个已经持有锁的线程再去获取这个锁,如果是普通的Lock,它会一直阻塞下去;但如果是稍微复杂一点的RLock,就可以顺利进行——这就是为什么它叫做可重入,因为持有这个锁的线程可以再次“进入”(获取锁)。简单来说,RLock会记录是哪个线程持有它,以及这个线程获取锁的次数,而普通的Lock则不会记录这些信息。

当有多个锁的时候,死锁问题就出现了,比如一个线程先尝试获取锁A,然后获取锁B,而另一个线程先获取锁B,再获取锁A。如果发生这种情况,最终你会发现第一个线程持有锁A,第二个线程持有锁B,而它们各自都在尝试获取对方持有的锁——这样就会导致两个线程都永远阻塞。

防止多个锁死锁的一种方法是确保无论哪个线程在获取锁时,锁的获取顺序都是一致的。然而,当每个实例都有自己的锁时,这样的组织方式就变得非常复杂和困难。

9

如果你在每个类里使用不同的锁对象,就有可能出现死锁的情况。比如说,一个操作先锁住了A,然后再锁住B,而另一个操作则是先锁住B,再锁住A,这样就会造成互相等待,谁也动不了。

如果你只用一个锁,那就意味着你的代码只能单线程执行,而有些操作其实可以并行进行。虽然在Python中,这个问题没有那么严重,因为Python本身就有一个全局锁,但如果你在写文件的时候持有全局锁,Python会释放这个全局锁,但其他所有的操作都会被阻塞。

所以这其实是一个权衡的问题。我建议使用小锁,这样可以最大化并行执行的机会,但要注意一次不要同时申请多个锁,也尽量不要长时间持有一个锁,除非真的必要。

就你提到的具体例子来说,第一个例子是有问题的。如果你对theList进行锁定操作,那么每次都必须使用同一个锁,否则就没有真正锁住任何东西。虽然在这里,list.append和list.remove操作本身是原子性的,不会有问题,但如果你确实需要锁定对列表的访问,就一定要确保每次都用同一个锁。最好的办法是把列表和锁作为一个类的属性,然后强制所有对列表的访问都通过这个类的方法来进行。这样,你就只需要传递这个包含类的对象,而不是列表或锁。

撰写回答