一、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的获取锁的顺序如何,先获取的线程会最终释放自己获取的锁,再次获取另一个锁。同时如果不成功获取另一个锁,则会等待一段时间后重试获取锁定,这样就可以避免死锁。