您的位置:

Python多线程锁:使用Lock实现线程同步的方法

一、Lock的作用及使用方法

在多线程中,为了防止数据错乱或者变量冲突,需要对线程进行同步。Python提供了多种同步方式,其中Lock是最基本的同步方法之一。

Lock对象可以看作是一个标志,表示只有当获取该标志的线程释放该标志之后,其他线程才能获取它。这样就可以保证同一时间只有一个线程访问共享资源。

使用Lock对象时,需要获取该对象的锁定,并在需要访问共享资源的代码块前调用acquire()方法,使用完后要利用release()方法释放锁定。

import threading
import time

class MyThread(threading.Thread):
    def __init__(self, thread_num, lock):
        threading.Thread.__init__(self)
        self.thread_num = thread_num
        self.lock = lock

    def run(self):
        print("Thread {} start".format(self.thread_num))
        self.lock.acquire()
        print("Thread {} acquired lock".format(self.thread_num))
        time.sleep(1)
        self.lock.release()
        print("Thread {} released lock".format(self.thread_num))

if __name__ == '__main__':
    lock = threading.Lock()
    threads = []
    for i in range(5):
        threads.append(MyThread(i, lock))
    for t in threads:
        t.start()
    for t in threads:
        t.join()

以上代码展示了Lock对象的使用方法。在这个例子中,创建了5个线程MyThread,每个线程都需要获取同一个Lock对象的锁定,在获取锁定后暂停1秒钟,然后释放锁定。

二、Lock的带计数器版本RLock

在Python中,还有一种类似Lock的带计数器版本,叫做RLock(R=Reentrant, 可重入)。相对于Lock而言,RLock多了一个计数器。同一个线程在多次调用acquire()方法后,只有在计数器为0的情况下,其他线程才能获取该锁。

可以通过调用Lock对象的方法获得RLock对象,就像下面这样:

import threading
import time

class MyThread(threading.Thread):
    def __init__(self, thread_num, lock):
        threading.Thread.__init__(self)
        self.thread_num = thread_num
        self.lock = lock

    def run(self):
        print("Thread {} start".format(self.thread_num))
        self.lock.acquire()
        print("Thread {} acquired lock".format(self.thread_num))
        time.sleep(1)
        self.lock.acquire()
        print("Thread {} acquired lock again".format(self.thread_num))
        time.sleep(1)
        self.lock.release()
        print("Thread {} released lock".format(self.thread_num))
        time.sleep(1)
        self.lock.release()
        print("Thread {} released lock again".format(self.thread_num))

if __name__ == '__main__':
    lock = threading.RLock()
    threads = []
    for i in range(5):
        threads.append(MyThread(i, lock))
    for t in threads:
        t.start()
    for t in threads:
        t.join()

在上述的代码中,MyThread线程在第一次获取RLock对象锁定后,调用了acquire()方法增加了计数器的值,然后又调用了一次acquire()方法,再次获取该锁定,计数器的值增加再次。通过两次计数器值的增加,就达到了可重入的目的。

三、使用Lock对象避免死锁

Lock对象虽然可以很好保证同步,但是如果在使用不当的情况下,可能会出现死锁的问题。比如,线程A获取了锁1,想要获取锁2;线程B获取了锁2,想要获取锁1,这时就出现了死锁。

避免死锁的方法是,在获取所有的锁前,先获取其中一个,如果没有获得,则释放已经获得的锁,等待一段时间后重试。这段等待的时间,可以通过调整参数来优化。

下面是一个典型的死锁示例:

import threading

def worker1(lock1, lock2):
    lock1.acquire()
    print("Worker1 acquired lock1")
    lock2.acquire()
    print("Worker1 acquired lock2")
    lock1.release()
    print("Worker1 released lock1")
    lock2.release()
    print("Worker1 released lock2")

def worker2(lock1, lock2):
    lock2.acquire()
    print("Worker2 acquired lock2")
    lock1.acquire()
    print("Worker2 acquired lock1")
    lock2.release()
    print("Worker2 released lock2")
    lock1.release()
    print("Worker2 released lock1")

if __name__ == '__main__':
    lock1 = threading.Lock()
    lock2 = threading.Lock()

    t1 = threading.Thread(target=worker1, args=(lock1, lock2))
    t2 = threading.Thread(target=worker2, args=(lock1, lock2))

    t1.start()
    t2.start()
    t1.join()
    t2.join()

以上代码演示了死锁。在该示例中,worker1和worker2都需要获取两个Lock对象的锁定,如果两个线程在获取锁定的顺序不同,则可能会出现死锁。在这种情况下,就需要按照特定的顺序获取锁定,避免死锁。比如下面的程序:

import threading
import time

def worker1(lock1, lock2):
    print("Worker1 waiting for lock1")
    lock1.acquire()
    print("Worker1 acquired lock1")
    time.sleep(1)
    while not lock2.acquire(blocking=False):
        print("Worker1 waiting for lock2")
        time.sleep(1)
    print("Worker1 acquired lock2")
    lock1.release()
    print("Worker1 released lock1")
    lock2.release()
    print("Worker1 released lock2")

def worker2(lock1, lock2):
    print("Worker2 waiting for lock1")
    lock2.acquire()
    print("Worker2 acquired lock2")
    time.sleep(1)
    while not lock1.acquire(blocking=False):
        print("Worker2 waiting for lock1")
        time.sleep(1)
    print("Worker2 acquired lock1")
    lock2.release()
    print("Worker2 released lock2")
    lock1.release()
    print("Worker2 released lock1")

if __name__ == '__main__':
    lock1 = threading.Lock()
    lock2 = threading.Lock()

    t1 = threading.Thread(target=worker1, args=(lock1, lock2))
    t2 = threading.Thread(target=worker2, args=(lock1, lock2))

    t1.start()
    t2.start()
    t1.join()
    t2.join()

在上述的程序中,不管worker1和worker2的获取锁的顺序如何,先获取的线程会最终释放自己获取的锁,再次获取另一个锁。同时如果不成功获取另一个锁,则会等待一段时间后重试获取锁定,这样就可以避免死锁。