系统的线程调度具有一定的随机性。
经典问题
银行取钱问题。
从银行取钱的基本流程基本上可以分为如下几个步骤:
- 用户输入账户、密码,系统判断用户的账户、密码是否匹配。
- 用户输入取款金额。
- 系统判断账户余额是否大于取款金额。
- 如果余额大于取款金额,则取款成功;如果余额小于取款金额,则取款失败。
代码:
import threading
import time
class Account:
# 定义构造器
def __init__(self, account_no, balance):
#封装账户编号、账户余额的两个成员变量
self.account_no = account_no
self.balance = balance
def draw(account, draw_amount):
if account.balance >= draw_amount:
# 吐出钞票
print(threading.current_thread().getName() \
+ "取钱成功!吐出钞票:" + str(draw_amount))
# 修改余额
account.balance -= draw_amount
print("\t 余额为:" + str(account.balance))
else:
print(threading.current_thread().getName() \
+ "取钱失败!余额不足!")
# 创建一个账户
acct = Account("001", 1000)
# 模拟两个线程对同一个账户取钱
threading.Thread(name='甲', target=draw, args=(acct, 800)).start()
threading.Thread(name='乙', target=draw, args=(acct, 800)).start()
结果,多数情况下为乙线程取钱失败,但是会出现都取钱成功,余额-600的情况。
为了防止这种错误的出现,就需要用到锁Lock。
acquire(blocking=True, timeout=-1)
请求对Lock或RLock加锁,其中timeout参数指定加锁多少秒。
Lock 和 RLock 的区别如下:
threading.Lock:它是一个基本的锁对象,每次只能锁定一次,其余的锁请求,需等待锁释放后才能获取。
threading.RLock:它代表可重入锁(Reentrant Lock)。对于可重入锁,在同一个线程中可以对它进行多次锁定,也可以多次释放。如果使用 RLock,那么 acquire() 和 release() 方法必须成对出现。如果调用了 n 次 acquire() 加锁,则必须调用 n 次 release() 才能释放锁。
release()
释放锁。
在实现线程安全的控制中,比较常用的是RLock。通常使用RLock的代码格式如下:
class X:
# 定义需要保证线程安全的方法
def m():
# 加锁
self.lock.acquire()
try:
#需要保证线程安全的代码
# ...方法体
# 使用finally块来保证释放锁
finally:
# 修改完成,释放锁
self.lock.release()
使用RLock对象来控制线程安全,当加锁和释放锁出现在不同的作用范围时,通常建议使用finally块来确保在必要时释放锁。
通过使用Lock对象可以非常方便地实现线程安全地类,线程安全的类具有如下特征:
- 该类的对象可以被多个线程安全地访问。
- 每个线程在调用该对象地任意方法之后,都将得到正确的结果。
- 每个线程在调用该对象的任意方法之后,该对象都依然保持合理的状态。
总体来说,不可变类总是线程安全的,因为它的对象状态不可改变。把上面银行问题的Account类改为如下形式,它就是线程安全的:
import threading
import time
class Account:
# 定义构造器
def __init__(self, account_no, balance):
# 封装账户编号、账户余额的两个成员变量
self.account_no = account_no
self._balance = balance
self.lock = threading.RLock()
# 因为账户余额不允许随便修改,所以只为self.__balance提供getter方法
def getBalance(self):
return self._balance
# 提供一个线程安全的draw()方法来完成取钱操作
def draw(self, draw_amount):
# 加锁
self.lock.acquire()
try:
# 账户余额大于取钱数目
if self._balance >= draw_amount:
# 吐出钞票
print(threading.current_thread().name \
+ "取钱成功!吐出钞票:" + str(draw_amount))
time.sleep(0.001)
# 修改余额
self._balance -= draw_amount
print("\t余额为:" + str(self._balance))
else:
print(threading.current_thread().name \
+ "取钱失败!余额不足!")
finally:
# 修改完成,释放锁
self.lock.release()
def draw(account, draw_amount):
# 直接调用account对象的draw方法来执行取钱操作
account.draw(draw_amount)
acct = Account('001', 1000)
# 模拟两个线程对同一个账户取钱
threading.Thread(name='甲', target=draw, args=(acct, 800)).start()
threading.Thread(name='乙', target=draw, args=(acct, 800)).start()
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略:
- 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。例如,上面 Account 类中的 account_no 实例变量就无须同步,所以程序只对 draw() 方法进行了同步控制。
- 如果可变类有两种运行环境,单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中使用钱程不安全版本以保证性能,在多线程环境中使用线程安全版本。
参考:
http://c.biancheng.net/view/2617.html