并发编程(三)Python编程慢的罪魁祸首。全局解释器锁GIL
并发编程(四)如何使用多线程,使用多线程对爬虫程序进行修改及比较
并发编程(七)好用的线程池ThreadPoolExecutor
并发编程(九)使用多进程multiprocessing加速程序运行
并发编程(十二)使用subprocess启动电脑任意程序(听歌、解压缩、自动下载等等)
线程安全概念介绍
-
线程安全是指某个函数、函数库在多线程环境被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
-
线程不安全是指由于线程的执行随时会发生切换,就造成了不可预料的结果,出现线程不安全。
# account:银行账户 # amount取钱金额 def draw(account, amount): if account.balance >= amount: account.balance -= amount
-
以上是一个取钱的一个函数方法,那如果将其放在多线程中会出现什么情况?(手机和银行同时取钱~)
-
假设该账户有1000元,此时两个线程A、B同时执行取钱任务,取800块钱。首先执行线程A判断1000大于800进入if判断,这时突然进行了线程切换,线程B开始执行,也判断1000大于800进入if判断。这时,再次切换到线程A,因为它已经进入if判断所以直接执行取钱就是1000-800。线程A结束后,线程B继续,因为它也已经进入if判断,所以它也会直接减去800,200-800=-600,就会出现bug造成很大的损失。而这种情况就是线程不安全问题,那么,该怎么解决这个问题呢?
Lock用于解决线程安全问题
-
从上文中我们已经简单了解线程不安全的问题,也遗留了怎么解决线程安全问题的问题。接下来,我们就通过两个方法来解决这个问题。
-
方法一:try-finally模式
# 导包 import threading # 创建锁 lock = threading.Lock() # 上锁 lock.acquire() # 开始执行任务 try: #do something pass # 执行完成之后释放锁 finally: lock.release()
-
方法二:with模式
# 导包 import threading # 创建锁 lock = threading.Lock() # 上锁执行任务,完成后释放锁 with lock: #do something
-
方法二是方法一的简写~
示例
-
在上文中我们提到了银行取钱的问题,接下来我就以银行取钱的代码为例来进行示例。
-
首先是不加lock的情况
# 导包 import threading import time # 银行账户类 class Account: def __init__(self, balance): self.balance = balance # 取钱方法 def draw(account, amount): if account.balance >= amount: # 进行时间睡眠,是为了进行线程切换 time.sleep(0.1) print(threading.current_thread().name, "取钱成功") account.balance -= amount print(threading.current_thread().name, "余额", account.balance) else: print(threading.current_thread().name, "取钱失败,余额不足,剩余", account.balance) if __name__ == '__main__': account = Account(1000) # 创建两个线程进行取钱 ta = threading.Thread(target=draw, args=(account, 800), name='ta') tb = threading.Thread(target=draw, args=(account, 800), name='tb') ta.start() tb.start() ''' output: tb 取钱成功 tb 余额 200 ta 取钱成功 ta 余额 -600 '''
-
接下来,我们在上面的程序中修改,加锁
# 导包 import threading import time # 创建锁 lock = threading.Lock() # 银行账户类 class Account: def __init__(self, balance): self.balance = balance # 取钱函数 def draw(account, amount): # 加锁 with lock: if account.balance >= amount: time.sleep(0.1) print(threading.current_thread().name, "取钱成功") account.balance -= amount print(threading.current_thread().name, "余额", account.balance) else: print(threading.current_thread().name, "取钱失败,余额不足,剩余", account.balance) if __name__ == '__main__': account = Account(1000) # 创建两个线程进行取钱 ta = threading.Thread(target=draw, args=(account, 800), name='ta') tb = threading.Thread(target=draw, args=(account, 800), name='tb') ta.start() tb.start() ''' output: ta 取钱成功 ta 余额 200 tb 取钱失败,余额不足,剩余 200 '''
-
通过两个代码的比较,你会发现两者就是在取钱函数中加了锁,加了锁以后。ta加锁进入if判断,这是切换线程进入tb,tb由于无法获取锁而不能继续进行取钱任务。这时,再次切换回ta线程,ta完成取钱释放锁。tb获取锁继续执行,判断为余额不足。