Python互斥锁解决线程安全问题

本文探讨了多线程并发的优势及数据同步问题,通过账户取钱案例展示了锁(尤其是RLock)在避免数据不同步中的关键作用,介绍了死锁现象及其解决方案。重点讲解了如何使用threading.RLock确保线程安全的取钱操作。
摘要由CSDN通过智能技术生成

线程是cpu调度的有一定的随机性,而多线程的优势在于并发性,可以同时运行多个任务。当线程需要共享数据时,可能会因为数据不同步而产生错误。

比如
多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。

考虑这样一种情况:一个列表里所有元素都是0,线程"set"从后向前把所有元素改成1,而线程"print"负责从前往后读取列表并打印。

那么,可能线程"set"开始改的时候,线程"print"便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。

锁有两种状态——锁定和未锁定。每当一个线程比如"set"要访问共享数据时,必须先获得锁定;如果已经有别的线程比如"print"获得锁定了,那么就让线程"set"暂停,也就是同步阻塞;等到线程"print"访问完毕,释放锁以后,再让线程"set"继续。

经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。

使用两个线程分别模拟两个人使用同一个账户做并发取钱操作:

import threading
import time

class Account:
    # 定义构造器
    def __init__(self, account_no, balance):
        # 封装账户编号、账户余额的两个成员变量
        self.account_no = account_no
        self.balance = balance

# 定义一个函数来模拟取钱操作
def Test(account, draw_amount):
    # 账户余额大于取钱数目
    if account.balance >= draw_amount:
        # 吐出钞票
        print(threading.current_thread().name\
            + "成功取出钞票:" + str(draw_amount))
        # 此处的延迟是认为切换其他子线程
        time.sleep(0.001)
        # 修改余额
        account.balance -= draw_amount
        print("\t余额为: " + str(account.balance))
    else:
        print(threading.current_thread().name\
            + "取钱失败!余额不足!")
# 创建一个账户
acct = Account("0000001" , 1000)
# 模拟两个线程对同一个账户取钱
threading.Thread(name='A', target=Test, args=(acct , 800)).start()
threading.Thread(name='B', target=Test, args=(acct , 800)).start()

结果
A成功取出钞票:800
B成功取出钞票:800
余额为: 200	余额为: -600

账户余额只有 1000 元时取出了 1600 元,而且账户余额出现了负值,远不是银行所期望的结果。虽然上面程序是人为地使用 time.sleep(0.001) 来强制线程调度切换,但这种切换也是完全可能发生的

Python互斥锁同步线程

出现以上错误结果是因为run()方法不具有线程安全性,程序中有两个并发线程在修改Account对象,而且系统在遇到耗时操作会执行线程切换,导致做了两次减的操作

为了解决这个问题,python的threading模块引入了互斥锁(Lock)和RLock两个类,两者都提供了下面两个方法加互斥锁和释放锁

  • acquire(timeout =0) 请求加锁,其中 timeout 参数指定加锁多少秒。
  • relaease() 执行释放锁

Lock 和 RLock 的区别如下:

  • lock 是一个基本的锁对象,每次只能锁定一次,其余的锁请求需要等待该锁释放后才能获取
  • RLock 代表重入锁。在同一个线程中可以对它进行多次锁定,也可以多次释放。如果使用RLock,那么acquire()和release()必须成对出现,如果调用了 n 次 acquire() 加锁,则必须调用 n 次 release() 才能释放锁。

Lock 是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程在开始访问共享资源之前应先请求获得 Lock 对象。当对共享资源访问完成后,程序释放对 Lock 对象的锁定。

在实现线程安全的控制中,比较常用的是 RLock。通常使用 RLock 的代码格式如下:

class X:
    #定义需要保证线程安全的方法
    def m () :
        #加锁
        lock.acquire()
        try :
        	pass
        #使用finally 块来保证释放锁
        finally :
            #修改完成,释放锁
            lock.release()

使用 RLock 对象来控制线程安全,当加锁和释放锁出现在不同的作用范围内时,通常建议使用 finally 块来确保在必要时释放锁。

import threading
import time
class Account:
    # 定义构造器
    def __init__(self, account_no, balance):
        # 封装账户编号、账户余额的两个成员变量
        self.account_no = account_no
        self._balance = balance
        self.lock = threading.RLock()
    # 提供一个线程安全的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("0000001" , 1000)
# 模拟两个线程对同一个账户取钱
threading.Thread(name='A', target=draw , args=(acct , 800)).start()
threading.Thread(name='B', target=draw , args=(acct , 800)).start()

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

线程每次开始执行 draw() 方法修改 self.balance 时,都必须先对 RLock 对象加锁。当该线程完成对 self._balance 的修改,将要退出 draw() 方法时,则释放对 RLock 对象的锁定。这样的做法完全符合“加锁→修改→释放锁”的安全访问逻辑。

上面程序中代表线程执行体的 draw() 函数无须自己实现取钱操作,而是直接调用 account 的 draw() 方法来执行取钱操作。由于 draw() 方法己经使用 RLock 对象实现了线程安全,因此上面程序就不会导致线程安全问题。

死锁

当两个线程互相等待对方释放资源时就会发生死锁。
一旦出现死锁整个程序既不会发生任何异常也不会给出任何提示,只是所有线程都处于阻塞状态,无法继续

解决方案:

  • 使用定时锁。程序在调用acquire()方法加锁可指定timeout参数,该参数指定超过timeout秒后自动释放对Lock的锁定,这样就可以解开死锁了
  • 具有相同的加锁顺序。如果多个线程需要对多个Lock锁定,则应该保证它们以相同的顺序请求加锁。
  • 避免多次锁定。尽量避免同一个线程对多个Lock进行锁定,比如
    主线程要对 A、B 两个对象的 Lock 进行锁定,子线程也要对 A、B 两个对象的 Lock 进行锁定,这就埋下了导致死锁的隐患。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

季布,

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值