通过Condition类实现线程同步
Condition类代表条件变量,它允许线程在触发某些事件或达到特定条件后才开始执行。通过Condition类提供的构造方法可以创建一个实例,该方法的声明如下:
Condition(lock = None )
以上构造方法中只有一个lock参数,该参数用于接收一个Lock对象或RLock对象。若没有为lock参数传入值,Condition对象会自动生成一个RLock对象。
Condition类中提供了与锁相关的acquire()和release()方法,这两个方法与Lock类中的用法一致,该类还提供了以下一些常用的方法:
wait(timeout=None):释放线程持有锁的同时挂起线程,直至线程接收到通知被唤醒或超时。
notify():唤醒一个处于挂起状态的线程。
notify_all() :唤醒所有处于挂起状态的线程
综上所述可知,Condition类中包含一个锁和一个等待池(放置处于阻塞状态的线程)。线程先调用acquire()方法对Condition对象的内部锁上锁,并判断其是否满足条件,具体分为以下两种情况:
(1) 若不满足条件,Condition对象调用wait()方法释放内部的锁,并将线程转换为阻塞状态,同时在等待池中记录处于阻塞状态的这个线程;
(2) 若满足条件,Condition对象调用notify()或notify_all() 方法唤醒等待池中的任意一条线程或所有线程,并将线程调用acquire()方法尝试将Condition对象的内部锁上锁。
假设某账户开户后余额为0,它具备支出和收入的统计功能。现在系统中分配了两个线程扮演“存款者”和“取款者”两个角色,分别执行向账户中存钱和取钱的行为:若当前账户的余额为0,先要求“取款者”处于等待状态,再要求“存款者”立即向账户中存钱;若当前账户中有余额,先要求“存款者”处于等待状态,再要求“取款者”立即从账户中取出所有的钱。“存款者”和“取款者”不允许连续两次存钱和取钱,并在执行完成后显示账户余额。下面使用程序来模拟以上场景,代码如下:
1 import threading
2 from threading import Thread, Condition
3 class Account(object):
4 def __init__(self, account_no, balance):
5 self.account_no = account_no # 账户编号
6 self._balance = balance # 账户余额
7 self.cond = Condition() # 创建表示条件变量的cond对象
8 self._flag = False # 标记是否已有存款
9 # 取钱操作
10 def draw_money(self, draw_amount):
11 # 加锁,相当于调用Condition绑定的Lock的acquire()
12 self.cond.acquire()
13 try:
14 # 如果self._flag为假,说明账户中还没有人存钱进去,取钱方法阻塞
15 if not self._flag:
16 self.cond.wait()
17 else:
18 # 执行取钱操作
19 print(threading.current_thread().name +
20 " 取钱:" + str(draw_amount))
21 self._balance -= draw_amount
22 print("账户余额为: " + str(self._balance))
23 # 将表示账户是否已有存款的标记设为False
24 self._flag = False
25 # 唤醒其他线程
26 self.cond.notify_all()
27 # 使用finally块来释放锁
28 finally:
29 self.cond.release()
30 # 存钱操作
31 def deposit(self, deposit_amount):
32 # 加锁,相当于调用Condition绑定的Lock的acquire()
33 self.cond.acquire()
34 try:
35 # 如果self._flag为真,说明账户中已有人存钱进去,存钱方法阻塞
36 if self._flag:
37 self.cond.wait()
38 else:
39 # 执行存款操作
40 print(threading.current_thread().name +
41 " 存钱:" + str(deposit_amount))
42 self._balance += deposit_amount
43 print("账户余额为: " + str(self._balance))
44 # 将表示账户是否已有存款的标记设为True
45 self._flag = True
46 # 唤醒其他线程
47 self.cond.notify_all()
48 # 使用finally块来释放锁
49 finally:
50 self.cond.release()
51 # 创建一个账户
52 acct = Account("123456" , 0)
53 for i in range(5):
54 # 创建、并启动一个“取钱”线程
55 threading.Thread(name="取款者", target=acct.draw_money,
56 args=(500,)).start()
57 # 创建、并启动一个“存款”线程
58 threading.Thread(name="存款者", target=acct.deposit,
59 args=(500,)).start()
以上代码中第3~50行是定义的Account类,该类中共包含三个方法:__ init__()、draw_money()和deposit()方法,分别用于账户初始化、取钱和存钱。
其中__ init__()方法中定义了4个属性:account_no、balance、cond和 flag。其中,_ balance表示账户余额;cond表示创建的Condition对象,用于在满足条件的情况下阻塞和唤醒线程;_ flag表示账户是否已有存款的标记,True为已有存款,False为没有存款。
draw_money()方法中,先对cond的内部锁进行锁定,之后使用if-else语句区分账户有存款和无存款两种情况,若not _ flag为True,说明账户中没有存款,需要通过“cond.wait()”将处于阻塞的线程放入等待池中;若not _ flag为False,说明账户中有存款,将账户余额_ balance全部取出,_ flag的标记修改为False,并通过“cond.notify_all()”将所有处于等待池中的线程唤醒。
同样,deposit()方法中,先对cond的内部锁进行锁定,之后使用if-else语句区分账户有存款和无存款两种情况,若_ flag为True,说明账户中有存款,通过“cond.wait()”将处于阻塞的线程放入等待池中;若_ flag为False,说明账户中没有存款,将账户余额_ balance重置为存款,_flag的标记修改为True,并通过“cond.notify_all()”将所有处于等待池中的线程唤醒。
第52~59行创建了一个余额为0的账户acct,也创建了5次名称为“存款者”和“取款者”的线程,分别执行代表存钱行为的acct.deposit和代表取钱行为的draw_money。
运行代码,结果如下所示。
存款者 存钱:500
账户余额为: 500
取钱者 取钱:500
账户余额为: 0
存款者 存钱:500
账户余额为: 500
取钱者 取钱:500
账户余额为: 0
存款者 存钱:500
账户余额为: 500
取钱者 取钱:500
账户余额为: 0
存款者 存钱:500
账户余额为: 500
取钱者 取钱:500
账户余额为: 0
存款者 存钱:500
账户余额为: 500
由以上结果可知,因为账户的余额为0,所以先执行了账户存钱的行为,打印“存款者 存钱:500”,之后又执行了账户取钱的行为,打印“取钱者 取钱:500”...... 共打印了5次“存款者 存钱:500”和5次“取钱者 取钱:500”。由此表明,程序通过Condition对象成功实现了线程间的同步。