线程安全概念

线程安全指在多线程环境中,函数或库能正确处理共享变量,确保程序功能正确执行。当多个线程并发操作共享资源时,若未同步执行,线程切换可能导致不可预料的结果。例如,在银行取款场景中,两个线程同时操作同一账户余额,可能因读取过时数据而出现负数余额,违背业务逻辑。

1、线程不安全示例

假设账户余额初始为 $1000$,两个线程均尝试取款 $800$:

  • 线程1执行流程
    1. 读取余额 $1000$,判断 $1000 \geq 800$(成立)。
    2. 执行扣减: $1000 - 800 = 200$,更新余额为 $200$。
  • 线程2执行流程
    1. 读取余额 $1000$(线程1操作前的值),判断 $1000 \geq 800$(成立)。
    2. 执行扣减: $200 - 800 = -600$,导致余额为负。

此问题源于临界区代码(如余额判断与扣减)未加锁,允许多线程并发执行,破坏数据一致性。


2、Lock解决方案

Python的 threading.Lock 通过互斥锁确保同一时间仅一个线程执行临界区代码。以下是两种标准用法:

1. try-finally 模式

显式管理锁生命周期:手动获取锁,在 try 块执行临界区代码,在 finally 块强制释放锁,避免死锁。

import threading
lock = threading.Lock()

lock.acquire()
try:
    # 临界区代码
    if account.balance >= amount:
        account.balance -= amount
finally:
    lock.release()

2. with 模式(推荐)

语法糖自动管理锁:进入 with 块时加锁,退出时解锁,代码简洁且避免漏释放。

import threading
lock = threading.Lock()

with lock:
    # 临界区代码
    if account.balance >= amount:
        account.balance -= amount


实例演示与测试

以下代码模拟银行取款场景,对比线程不安全与安全方案:

代码实现
# 1. 导入Python自带的多线程工具:用来创建多个同时运行的线程
import threading
# 2. 导入Python自带的时间工具:用来让程序暂停一会儿
import time

# 3. 定义一个账户类:模拟银行的账户,用来存余额
class Account:
    # 4. 初始化方法:创建账户时,给账户设置初始余额
    def __init__(self, balance=0):
        # 5. 把传入的余额,存到账户对象里(这是共享的钱,多个线程都会操作它)
        self.balance = balance  

# 6. 定义【不安全】的取款函数:会出现超支bug
# account:要操作的账户,amount:要取多少钱
def unsafe_draw(account, amount):
    # 7. 判断:账户里的钱 >= 要取的钱吗?
    if account.balance >= amount:
        # 8. 程序暂停0.1秒:故意制造线程切换,触发线程安全问题
        time.sleep(0.1)
        # 9. 扣钱:账户余额 = 余额 - 取款金额
        account.balance -= amount
        # 10. 打印提示:取款成功,显示当前余额
        print(f"取款成功!当前余额:{account.balance}")
    # 11. 如果余额不够,执行这里
    else:
        # 12. 打印提示:余额不足,显示当前余额
        print(f"余额不足!当前余额:{account.balance}")

# 13. 创建一把线程锁:用来保护账户钱不被乱扣(同一时间只允许一个线程操作)
lock1 = threading.Lock()
# 14. 定义【第一种安全】的取款函数:用try-finally管理锁
def safe_draw_try(account, amount):
    # 15. 手动加锁:抢到锁的线程才能往下执行,没抢到的等着
    lock1.acquire()
    try:
        # 16. 判断余额够不够取款
        if account.balance >= amount:
            # 17. 暂停0.1秒,模拟业务延迟
            time.sleep(0.1)
            # 18. 安全扣钱
            account.balance -= amount
            # 19. 打印取款成功提示
            print(f"try方案:取款成功!当前余额:{account.balance}")
        # 20. 余额不足时执行
        else:
            # 21. 打印余额不足提示
            print(f"try方案:余额不足!当前余额:{account.balance}")
    # 22. 无论代码是否报错,最后一定会执行这里
    finally:
        # 23. 手动释放锁:让其他线程可以继续使用
        lock1.release()

# 24. 创建第二把线程锁(和lock1功能一样,分开测试用)
lock2 = threading.Lock()
# 25. 定义【第二种安全】的取款函数:用with自动管理锁(推荐!)
def safe_draw_with(account, amount):
    # 26. with lock:自动加锁,代码执行完自动解锁,不用手动操作
    with lock2:
        # 27. 判断余额够不够
        if account.balance >= amount:
            # 28. 暂停0.1秒
            time.sleep(0.1)
            # 29. 安全扣钱
            account.balance -= amount
            # 30. 打印取款成功提示
            print(f"with方案:取款成功!当前余额:{account.balance}")
        # 31. 余额不足时执行
        else:
            # 32. 打印余额不足提示
            print(f"with方案:余额不足!当前余额:{account.balance}")

# ------------------- 测试代码:开始运行程序 -------------------
# 33. 程序主入口:只有直接运行这个文件时,才会执行下面的代码
if __name__ == '__main__':
    # 34. 创建一个账户对象,初始余额1000元
    account = Account(balance=1000)
    # 35. 定义每次取款的金额:800元
    amount = 800  

    # 36. 打印分割线:提示开始测试【不安全】取款
    print("===== 测试:线程不安全(会超支)=====")
    # 37. 创建第一个线程:专门执行不安全的取款方法
    t1 = threading.Thread(target=unsafe_draw, args=(account, amount))
    # 38. 创建第二个线程:同时执行不安全的取款方法
    t2 = threading.Thread(target=unsafe_draw, args=(account, amount))
    # 39. 启动第一个线程,开始取款
    t1.start()
    # 40. 启动第二个线程,开始取款
    t2.start()
    # 41. 等待第一个线程执行完,再往下走
    t1.join()
    # 42. 等待第二个线程执行完,再往下走
    t2.join()
    # 43. 打印最终的账户余额
    print(f"最终账户余额:{account.balance}\n")

    # 44. 重置账户余额:把钱改回1000元,继续测试安全方案
    account.balance = 1000
    # 45. 打印分割线:提示开始测试【try-finally安全方案】
    print("===== 测试:Lock try-finally 安全方案 =====")
    # 46. 创建线程1,执行第一种安全取款
    t1 = threading.Thread(target=safe_draw_try, args=(account, amount))
    # 47. 创建线程2,执行第一种安全取款
    t2 = threading.Thread(target=safe_draw_try, args=(account, amount))
    t1.start()  # 启动线程1
    t2.start()  # 启动线程2
    t1.join()   # 等待线程1结束
    t2.join()   # 等待线程2结束
    print(f"最终账户余额:{account.balance}\n")  # 打印最终余额

    # 48. 再次重置余额为1000元,测试第二种安全方案
    account.balance = 1000
    # 49. 打印分割线:提示开始测试【with自动锁安全方案】
    print("===== 测试:Lock with 安全方案(推荐)=====")
    # 50. 创建线程1,执行第二种安全取款
    t1 = threading.Thread(target=safe_draw_with, args=(account, amount))
    # 51. 创建线程2,执行第二种安全取款
    t2 = threading.Thread(target=safe_draw_with, args=(account, amount))
    t1.start()  # 启动线程1
    t2.start()  # 启动线程2
    t1.join()   # 等待线程1结束
    t2.join()   # 等待线程2结束
    print(f"最终账户余额:{account.balance}")  # 打印最终余额
测试输出
===== 测试:线程不安全(会超支)=====
取款成功!当前余额:200
取款成功!当前余额:-600
最终账户余额:-600

===== 测试:Lock try-finally 安全方案 =====
try方案:取款成功!当前余额:200
try方案:余额不足!当前余额:200
最终账户余额:200

===== 测试:Lock with 安全方案(推荐)=====
with方案:取款成功!当前余额:200
with方案:余额不足!当前余额:200
最终账户余额:200


结论

  1. 线程不安全场景:未加锁时,并发操作导致余额超支($-600$)。
  2. Lock 效果
    • try-finally 和 with 方案均确保余额正确($200$)。
    • with 模式更简洁,推荐优先使用。

通过锁机制,可保证共享变量操作的原子性,避免多线程竞争问题。实际开发中,需对所有临界区代码加锁,确保全局一致性。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐