Python并发编程:线程锁、可重入锁与死锁详解

Ruby
Ruby
发布时间:2026-10-14| 2人浏览过
原创

本文深入解析Python中的线程锁机制,通过库存扣减示例演示Lock与RLock的使用场景。重点探讨并发环境下数据一致性问题,分析普通锁导致的死锁成因,并给出使用可重入锁(RLock)及with语句的最佳实践方案,帮助开发者避免常见的并发陷阱。

线程锁的基本原理

若具备操作系统基础,对“锁”的概念应不陌生。线程锁的核心作用在于确保特定逻辑或资源在同一时刻仅能被单一线程访问。这一机制类似于房间被锁住,唯有持有钥匙者方可进入;用户进出房间时需归还钥匙,供后续使用者获取。在此隐喻中,“房间”代表受保护的资源或代码段,“持钥匙者”即执行操作的线程。

为何需要加锁

加锁的原因

理解锁的原理后,一个关键问题浮现:为何必须使用锁?其典型应用场景有哪些?

锁的应用极为广泛,以电商购买为例。商家库存有限,每售出一件库存减一。假设某商品仅剩一件,此时两个用户同时发起购买请求。若未加锁,两个线程可能同时读取到库存为1且大于0,随后均执行减一操作。由于并发执行,最终库存可能变为-1,这显然不符合逻辑。

库存出现负数显然是错误的,因此必须避免此类竞态条件。通过加锁机制,可强制同一时刻仅允许一个线程发起购买请求。当第一个线程将库存减至0后,第二个线程将无法修改数据,从而保证数据的一致性与准确性。

Python中锁的代码实现

代码实现

在Python中,如何实现上述锁机制?

实现过程十分简便。Python标准库threading已提供线程工具,直接调用即可。通过实例化threading.Lock对象,可轻松实现方法级别的加锁功能。


import threading

class PurchaseRequest:
  '''
  初始化库存与锁
  '''
  def __init__(self, initial_value = 0):
    self._value = initial_value
    self._lock = threading.Lock()

  def incr(self,delta=1):
    '''
    加库存
    '''
    self._lock.acquire()
    self._value += delta
    self._lock.release()

  def decr(self,delta=1):
    '''
    减库存
    '''
    self._lock.acquire()
    self._value -= delta
    self._lock.release()

从代码可见,Lock对象的使用逻辑清晰:在进入临界区(资源竞争区)前,需调用lock.acquire()获取锁。Lock对象确保同一时刻仅有一个线程能成功获取锁,随后方可继续执行。任务完成后,需调用release方法“归还”锁,以便其他线程获取。

此处存在一个常见隐患:许多程序员在编码时容易遗忘调用release方法,导致锁泄露并引发难以排查的Bug。此类分布式或并发场景下的错误往往难以通过常规测试发现,因为并发条件在测试环境中较难模拟;代码审查时也易被忽略,一旦上线便难以定位。

为解决此问题,Lock提供了改进用法:结合with语句。此前在文件操作中,with语句已用于自动处理try-finally及资源回收。同理,使用with语句管理锁时,无需手动申请与释放,代码更为简洁。上述代码可重构如下:


import threading

class PurchaseRequest:
  '''
  初始化库存与锁
  '''
  def __init__(self, initial_value = 0):
    self._value = initial_value
    self._lock = threading.Lock()

  def incr(self,delta=1):
    '''
    加库存
    '''
 with self._lock:
     self._value += delta

  def decr(self,delta=1):
    '''
    减库存
    '''
    with self._lock:
     self._value -= delta

如此改写后,代码结构是否更加清晰整洁?

可重入锁与死锁处理

可重入锁

前述示例仅涉及最基础的锁机制,实际开发中更常使用的是可重入锁。

何谓可重入锁?简言之,若某线程已持有锁,它仍可再次进入被该锁保护的代码区域。既然线程未释放锁,为何需要再次进入?这通常源于递归调用的场景。

对前述示例稍作修改,结果将截然不同。


import threading

class PurchaseRequest:
  '''
  初始化库存与锁
  '''
  def __init__(self, initial_value = 0):
    self._value = initial_value
    self._lock = threading.Lock()

  def incr(self,delta=1):
    '''
    加库存
    '''
 with self._lock:
     self._value += delta

  def decr(self,delta=1):
    '''
    减库存
    '''
    with self._lock:
     self.incr(-delta)

观察decr方法,此处通过调用incr实现减库存逻辑。然而,decr本身也是加锁方法,按普通锁逻辑需等待前一个锁释放后方可进入。由于线程已持有锁且未释放,再次尝试获取同一把锁时,便会陷入死锁状态。

解决此问题的方案是将Lock替换为可重入锁(RLock),仅需修改一行代码即可生效。


import threading

class PurchaseRequest:
  '''
  初始化库存与锁
  我们使用RLock代替了Lock,也可重入锁代替了普通锁
  '''
  def __init__(self, initial_value = 0):
    self._value = initial_value
    self._lock = threading.RLock()

  def incr(self,delta=1):
    '''
    加库存
    '''
 with self._lock:
     self._value += delta

  def decr(self,delta=1):
    '''
    减库存
    '''
    with self._lock:
     self.incr(-delta)

总结与展望

总结

本文介绍了Python中锁的基本用法及可重入锁的概念。在并发场景下,开发与调试极具挑战性,稍有不慎便可能陷入各类陷阱。死锁仅是其中一种常见且相对易解的问题,此外还存在其他复杂的并发异常。

针对死锁及其他并发问题,Python提供了多种解决方案,将在后续文章中深入探讨。

以上为Python并发锁与死锁问题的详细解析,更多关于Python并发锁与死锁的资料,敬请参阅本站其他相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。