暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Python多线程的安全问题

AI有温度 2021-08-24
793
B站同名【有温度的算法】已经上线
想观看视频讲解的同学
在文末点击【原文链接】直达B站
不了解多线程的同学可以先阅读
Python多线程的使用
Python线程池的使用

锁通常用来解决程序在使用多线程时因共同访问共享资源所出现的线程安全问题。比如火车站有10个窗口卖火车票,卖完票后则会显示票的余量为0,假设A窗口在卖最后一张票时,另外9个窗口也同时销售最后一张票,那么库存最后会显示为-9,显然不符合正常情况。所以在A窗口卖最后一张票时,从购买到支付完成的这个短暂过程中,其他窗口都不能卖这张票

在这个例子中,10个窗口就好比10个线程,余票就好比共享资源,大家都可以对其进行访问。但是对于每一张票的售卖,规定同一时间段只能有一个窗口进行操作,否则就会出现错误。这个规定就是代码中的锁。
我们把一个时间段内只允许一个线程使用的资源称为临界资源,对临界资源的访问,必须互斥的进行。互斥,也称间接制约关系。线程互斥指当一个线程访问某临界资源时,另一个想要访问该临界资源的线程必须等待。当前访问临界资源的线程访问结束,释放该资源之后,另一个线程才能去访问临界资源。锁的功能就是实现线程互斥。

银行取款

关于锁的使用,有一个经典的“银行取款”的例子。从银行取钱的流程基本上可以分为如下步骤:

  • 用户输入账户、密码,系统判断用户的账户、密码是否匹配

  • 用户输入取款金额

  • 系统判断账户余额是否大于取款金额

  • 如果余额大于取款金额,则取款成功;如果余额小于取款金额,则取款失败

这就是日常生活中的取款流程。但这个流程如果不使用锁,放在多线程并发的场景下,就有可能出现问题。按照上面的流程编写取款程序,并使用两个线程来模拟两个人同时向一个账户并发取钱。此处忽略检查账户和密码的操作,仅仅模拟后三步操作。

先定义一个账户类,该账户类封装了账户ID和账户余额两个变量。

class Account:
def __init__(self, card_id, balance):
        # 封装账户ID、账户余额的两个变量
        self.card_id= card_id
self.balance = balance

接下来定义一个模拟取钱的函数,该函数根据账户、取款金额进行取钱操作,取钱的逻辑是当账户余额不足时无法提取现金;当余额足够时系统吐出钞票,余额减少。我们创建一个账户,并启动两个线程从该账户中取钱。代码如下:

import threading
import time
# 定义一个函数来模拟取钱操作
# 第一个参数为要操作的账户,第二个参数为索取金额
def withdraw(account, money):
# 账户余额大于取钱数目
if account.balance >= money:
# 吐出钞票
print(threading.current_thread().name + "取钱成功!吐出钞票:" + str(money),end=' ')
# time.sleep(0.1)
# 修改余额
account.balance -= money
print("\t余额为: " + str(account.balance))
else:
print(threading.current_thread().name + "取钱失败!余额不足")


# 创建一个账户,银行卡id为8888,存款1000元
acct = Account("8888" , 1000)


# 模拟两个对同一个账户取钱
threading.Thread(name='窗口A', target=withdraw , args=(acct , 800)).start()
threading.Thread(name='窗口B', target=withdraw , args=(acct , 800)).start()

我们首先创建一个账户,银行卡id为8888,存款1000元,然后模拟两个窗口同时对8888的银行卡进行取款,取款金额都为800元。正常情况下,只有一个账户可以取款成功,会打印:

窗口A取钱成功! 吐出钞票:800 余额为: 200
窗口B取钱失败!余额不足

但是我们多次运行后,会看到如下图所示的错误结果:

窗口A取钱成功!吐出钞票:800 
窗口B取钱成功!吐出钞票:800
余额为: 200
余额为: -600

账户余额只有 1000 元,但却取出了 1600 元,账户余额成为了负值,这肯定不是银行所期望的结果。原因就是线程调度的不确定性,使得多线程在同一时段对都访问了临界资源,就是账户余额。如果我们把第10行代码time.sleep(0.1)取消注释,那么在一个线程sleep时,系统就会切换到另一个窗口对账户进行取款操作,这时第一个线程只判断了取款金额是否小于存款金额,还没对存款金额进行扣除,第二个线程就对账户进行了访问,它们同时访问临界资源,出现如上错误。

所以我们需要使得取款这一过程,在一段时间内只能被一个线程所访问,当这个线程访问结束后,另一线程才可继续访问。

同步锁

为了解决这个问题,Python 的 threading 模块引入了锁。threading 模块提供了 Lock 类,它有如下方法加锁和释放锁:

  • acquire(blocking=True, timeout=-1):对 Lock加锁,其中timeout参数指定加锁多少秒

  • release():释放锁

class Account:
def __init__(self, card_id, balance):
# 封装账户ID、账户余额的两个变量
        self.card_id= card_id
self.balance = balance


def withdraw(account, money):
    # 进行加锁
lock.acquire()
# 账户余额大于取钱数目
if account.balance >= money:
# 吐出钞票
        print(threading.current_thread().name + "取钱成功!吐出钞票:" + str(money),end=' ')
# 修改余额
account.balance -= money
print("\t余额为: " + str(account.balance))
else:
print(threading.current_thread().name + "取钱失败!余额不足")
# 进行解锁
lock.release()
# 创建一个账户,银行卡id为8888,存款1000元
acct = Account("8888" , 1000)


# 模拟两个对同一个账户取钱
# 在主线程中创建一把锁
lock = threading.Lock()
threading.Thread(name='窗口A', target=withdraw , args=(acct , 800)).start()
threading.Thread(name='窗口B', target=withdraw , args=(acct , 800)).start()
我们首先在主线程中创建一把锁lock = threading.Lock(),然后在开始进行取钱时,就对此过程进行加锁lock.acquire(),在一次取钱完成后,再进行解锁lock.release()。在acquire()与release()中间的操作,只允许一个线程进行访问,只有当这个线程结束之后,才允许别的线程继续访问。这样的做法完全符合“加锁→修改→释放锁”的安全访问逻辑。
这意味着,并发线程在任意时刻只有一个线程可以访问临界区域进行操作,所以我们必须让同一时刻最多只有一个线程处于临界区内,从而保证线程安全。
获取本章代码与本文集合,请在后台回复【线程安全】即可获取。
点击【阅读全文】直达B站

文章转载自AI有温度,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论