news 2026/10/7 2:12:50

gevent.lock 并发原语全解析:Semaphore、BoundedSemaphore、DummySemaphore 与 RLock 的源码级指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
gevent.lock 并发原语全解析:Semaphore、BoundedSemaphore、DummySemaphore 与 RLock 的源码级指南
  • 后端

【免费下载链接】gevent

Coroutine-based concurrency library for Python

项目地址:https://gitcode.com/gh_mirrors/ge/gevent
点击查看免费下载

导读

gevent.lock是 gevent 提供的协程级锁原语模块,为绿色协程(greenlet)场景重新实现了信号量(Semaphore)与可重入锁(RLock),同时针对多线程混用、公平唤醒、C 扩展加速等做了专门的工程化处理。阅读完本文,你将掌握四大并发原语的完整 API、上下文管理器用法、在 gevent 自身(线程池、连接池、文件对象)中的实际应用模式,以及其与标准库threading锁在实现与语义上的关键差异。

说明:docs/api/gevent.lock.rst是本文的主体依据,它通过automodule自动渲染 src/gevent/lock.py 中的类文档;文中所有 API 细节、版本变更与实现原理均以该模块及 src/gevent/_semaphore.py 的源码与 src/gevent/tests/test__semaphore.py、src/gevent/tests/test__lock.py 的测试为准。

模块概览:四个公开原语

gevent.lock通过__all__导出四个公开类(见 src/gevent/lock.py):

类语义适用场景
Semaphore任意上限的信号量,计数 = release 次数 − acquire 次数 + 初始值限制并发访问有限资源(默认 value=1 时为互斥锁)
BoundedSemaphore更安全的信号量,防止过度 release大多数用户应优先选择,能提前暴露计数失衡的 bug
DummySemaphore“无限容量”的信号量,任何方法都永不阻塞参数化“是否需要真正加锁”,资源无上限或对象本身线程安全时使用
RLock可重入互斥锁,API 与threading.RLock一致同一 greenlet 需要多次进入临界区

需要特别强调的是:所有 gevent 内部代码、测试乃至用户代码,都只能从gevent.lock导入这些原语,而绝不要直接导入gevent._semaphore。后者仅作为 Cython 可单独编译的实现单元存在,其模块头注释明确写道“This is not the place to import from”(见 src/gevent/_semaphore.py);测试文件 src/gevent/tests/test__semaphore.py 也重复了这一约定。

Semaphore:基础信号量

构造与语义

from gevent.lock import Semaphore s = Semaphore(value=1) # value 默认为 1

信号量维护一个计数器,其值等于release调用次数减去acquire调用次数,再加上初始值。acquire在必要时会阻塞,直到返回时计数器不会变为负数。与线程锁不同,信号量不追踪 greenlet 所有权——任何 greenlet 都可以调用release,即使它从未acquire过(见 src/gevent/_semaphore.py)。

构造时如果value为负数会抛出ValueError("semaphore initial value must be >= 0")(src/gevent/_semaphore.py)。

核心方法

acquire(blocking=True, timeout=None) -> bool

  • blocking=True(默认)时阻塞直到获得信号量;
  • timeout为浮点秒数,指定最大阻塞时间(仅当blocking=True时有效);
  • 返回值:True表示成功获取;如果blocking=True, timeout=None且初始值大于 0,则永远返回True;若超时未获得则返回False。注意极端情况:若别的调用方已经启动了计时器,本方法仍可能抛出Timeout异常(src/gevent/_semaphore.py);
  • 若以value=0初始化且blocking=True, timeout=None,将永久阻塞(除非有超时或blocking=False)。

release()

计数器加 1,并通知等待者;无返回值。文档明确提示:过度 release 是允许的(释放次数超过 acquire 次数与初始值之和),这通常是 bug 的征兆,但在某些场景下可被刻意利用,例如“建模额外资源的到达”(src/gevent/_semaphore.py)。

wait(timeout=None) -> int

等待直到可以获取信号量(或超时)。返回值是一个整数,表示在阻塞之前还能获取多少次——这个数字可能是 0(例如其他等待者抢到了)。若以value=0初始化且不传超时,将永远阻塞(src/gevent/_semaphore.py)。

状态查询

  • locked():返回信号量是否“不可获取”,即counter <= 0(对二进制信号量最有用);
  • ready():返回counter > 0,即是否可以获取(src/gevent/_semaphore.py)。

上下文管理器

信号量实现了__enter__/__exit__,可直接用于with语句:

from gevent.lock import Semaphore sem = Semaphore(2) with sem: # 进入时 acquire,退出时 release # 最多允许 2 个 greenlet 同时进入 pass

一个值得一提的细节:该类的__exit__在 CPython 下不会调用 trace 函数,但在 PyPy 下会(src/gevent/_semaphore.py)。

等待者唤醒顺序与公平性

唤醒顺序的语义在历次版本中有明确变更:

  • 1.4.0 起文档明确“等待者唤醒顺序未指定”;此前因 CPython 实现细节通常表现为 FIFO;
  • 1.5a3 起,等待中的 greenlet 按等待顺序(FIFO)被唤醒(src/gevent/_semaphore.py);
  • 1.5a3 同时规定底层rawlink方法会在调用回调前自动 unlink 等待者。

这一公平性有测试直接验证:TestSemaphoreFair.test_fair_or_hangs(见 src/gevent/tests/test__semaphore.py)构造了三个绿色协程相互链式 acquire/release 的竞争场景,如果锁不公平就会在最后两个 greenlet 之间无限自旋(对应 issue #1487),测试通过预期抛出LoopExit并核对各 greenlet 的存活状态来确认公平性。

多线程使用的演进(20.12.0 / 24.2.1)

gevent.lock.Semaphore不仅服务于单线程内的绿色协程切换,还经过了专门的跨线程加固:

  • 20.12.0:改进多线程使用支持。检测到跨线程使用时,实例不再为不存在的线程 hub 强行创建它(src/gevent/_semaphore.py)。这一改动至关重要,因为 Semaphore 被importlib.ModuleLock使用,而后者在导入 hub 本身的过程中就会出现——绝不能在此处强制创建 hub(src/gevent/_semaphore.py);
  • 24.2.1:跨线程操作改用 Python 3 原生锁超时机制,取代原先的自旋等待(src/gevent/_semaphore.py)。

从实现看,acquire会记录首次获取它的线程标识(_multithreaded),一旦发现不同线程访问便标记为_MULTI;跨线程阻塞时根据“是否存在两个 hub”选择不同的路径:双 hub 场景用 async watcher 通知(__acquire_using_two_hubs),无 hub 场景用原生线程锁(__acquire_without_hubs),单 hub 场景则通过run_callback_threadsafe委托给归属 hub(src/gevent/_semaphore.py)。

对应的多线程测试位于 src/gevent/tests/test__semaphore.py,包括test_acquire_in_one_then_another(主线程持有、工作线程等待后释放)、test_acquire_in_one_then_another_timed(等待线程超时放弃)、test_dueling_threads与test_dueling_threads_with_hub(两个线程无其他绿色协程可切换时反复 acquire/release 一万次),以及TestBoundedSemaphoreMultiThread对 Bounded 版本的同样验证。

关于 C 扩展与纯 Python 实现

gevent._semaphore是可通过 Cython 单独编译的.py文件(编译产物为gevent._gevent_c_semaphore)。文件末尾通过import_c_accel尝试加载 C 加速版本(src/gevent/_semaphore.py)。测试 src/gevent/tests/test__semaphore.py 会校验在非纯 Python 模式下Semaphore.__module__确为gevent._gevent_c_semaphore。

在纯 Python 模式(PURE_PYTHON)或 PyPy 下,gevent.lock会改用它内部的_AtomicSemaphore/_AtomicBoundedSemaphore包装类,通过自有的_GILLock模拟“方法全程持有 GIL”的原子性语义,确保与 Cython 版本行为一致(见 src/gevent/lock.py 与 src/gevent/lock.py)。注释中还记录了 PyPy 下纯 Python 版本比 Cython 版本快数倍这一反直觉现象(micro-benchmark 约 1s 对 4s),以及历史上 Cython 版本在 PyPy ≤ 4.0.1 的 GC 交互崩溃问题(src/gevent/_semaphore.py)。

BoundedSemaphore:防过度释放的安全信号量

from gevent.lock import BoundedSemaphore bs = BoundedSemaphore(value=1) # value 默认为 1

BoundedSemaphore检查当前值不会超过初始值:若过度释放(当前值已达到初始值仍调用release),抛出ValueError(src/gevent/_semaphore.py)。由于大多数场景中信号量用于守护有限容量的资源,释放次数超出 acquire 次数 + 初始值,通常是明显的编程 bug——这正是官方建议“拿不准时优先选择 BoundedSemaphore”的原因(src/gevent/_semaphore.py)。

两个值得注意的实现细节:

  • _OVER_RELEASE_ERROR类属性用于 monkey-patching 时替换抛出的异常类型(src/gevent/_semaphore.py);
  • _at_fork_reinit在 fork 后会把计数器重置为初始值,配合BoundedSemaphore的 hub 释放逻辑(release后若计数器回到初始值则解除 hub 绑定)提升跨线程/跨进程使用安全(src/gevent/_semaphore.py)。

上下文管理器与线程安全同样适用:BoundedSemaphore继承自Semaphore,具备相同的with用法、wait/locked/ready方法与多线程支持。

DummySemaphore:永不阻塞的“替身锁”

from gevent.lock import DummySemaphore d = DummySemaphore(value=None) # 1.1rc3 起接受并忽略 value 参数以兼容 Semaphore

DummySemaphore拥有与Semaphore相同的 API,但以“无限”初始值初始化,任何方法都永不阻塞(见 src/gevent/lock.py 的完整文档):

  • locked()永远返回False;ready()永远返回True;
  • release()什么也不做;wait(timeout=None)立即返回 1;
  • acquire(blocking=True, timeout=None)忽略所有参数,1.1a1 起始终返回True;
  • 作为上下文管理器使用时,进入/退出都是空操作。

它的核心价值在于参数化“是否真的需要加锁”(src/gevent/lock.py):

  1. 资源确实有限(如固定大小的线程池)→ 用真正的Semaphore;资源无上限 → 用DummySemaphore,从而支持代码完全不变;
  2. 底层对象已知线程安全、无需互斥 → 用DummySemaphore;否则用真正的Semaphore。

gevent 内部正是如此使用的(src/gevent/lock.py):

  • src/gevent/pool.py:Pool在size=None(不限制大小)时用DummySemaphore作为_semaphore,否则用Semaphore(size);
  • src/gevent/_fileobjectcommon.py:FileObject系列根据参数决定用Semaphore()还是DummySemaphore()来保护底层文件 I/O,文档也明确允许用户传入自己的gevent.lock.Semaphore实例(src/gevent/_fileobjectcommon.py)。

RLock:可重入互斥锁

from gevent.lock import RLock lock = RLock(hub=None) # hub 参数 20.5.1 起可用 with lock: with lock: # 同一 greenlet 可重复进入 pass

RLock是“同一 greenlet 可多次 acquire 的互斥锁”:

  • 任意时刻只能被一个 greenlet 持有;
  • 同一个 greenlet 可以多次acquire,但每次acquire必须配对一次release;
  • 未 acquire 过该锁的 greenlet 调用release是错误,会抛出RuntimeError;
  • 实例是上下文管理器(src/gevent/lock.py)。

方法细节

acquire(blocking=True, timeout=None)

  • blocking=True时阻塞,最长timeout秒(timeout 参数 1.5a4 起新增);
  • 返回布尔值表示是否获取成功;
  • 若当前 greenlet 已是持有者,则内部计数_count加 1 并立即返回 1(可重入的核心逻辑,见 src/gevent/lock.py)。

release()

  • 只有最初获取锁的 greenlet 可以释放,否则抛出RuntimeError("cannot release un-acquired lock. ...");
  • 内部计数减到 0 时清除持有者并释放底层信号量(src/gevent/lock.py)。

locked()

  • 返回当前是否被锁定(_count > 0),该方法为25.4.1 版本新增(src/gevent/lock.py)。

实现方式与内部接口

RLock的实现非常简洁:内部委托给一个Semaphore(1, hub)作为互斥基元,外加_owner(当前持有 greenlet)与_count(重入深度)两个状态(src/gevent/lock.py)。因此它天然继承了 Semaphore 的协程友好与多线程安全特性。

它还暴露了三个“供条件变量内部使用”的方法:_acquire_restore(count_owner)、_release_save()与_is_owned(),用于在不丢失锁状态的前提下临时保存/恢复所有权(src/gevent/lock.py)。

多线程场景下,RLock复用信号量的跨线程测试:TestRLockMultiThread直接继承test__semaphore.TestSemaphoreMultiThread(见 src/gevent/tests/test__lock.py),并特意不在测试前显式设置 hub,以覆盖“后台线程首次接触锁时才决定归属”的竞态路径。fork 场景则由TestLockReinitAfterFork覆盖:通过子进程脚本在回调运行期间 fork 并反复 acquire/release(对应 issue #1895),验证不再出现断言失败输出(src/gevent/tests/test__lock.py)。

在 gevent 内部的使用

RLock被 gevent 自身广泛使用:

  • src/gevent/threading.py 与 src/gevent/builtins.py 引入RLock,作为threading.RLock的绿色协程替代品参与 monkey-patching;
  • 从源码结构看,凡是需要在同一 greenlet 内递归获取的锁场景(如重入保护的资源访问),都以RLock为首选。

在真实项目中的典型应用模式

1. 连接/资源池限流(Pool)

src/gevent/pool.py 展示了最经典的信号量用法:Pool(size)用Semaphore(size)控制最大并发 greenlet 数,size=None时不限流并退化为DummySemaphore;size=0则创建一个永远无法 spawn 的池(除非配合wait_available超时与free_count检查)。wait_available的实现本质上就是等待信号量可获取。

2. 写锁与线程池配额

  • src/gevent/server.py:StreamServer用Semaphore()(二进制信号量)实现写锁_writelock;
  • src/gevent/threadpool.py:ThreadPool导入Semaphore限制排队任务配额(其注释同时提醒:gevent.lock.Semaphore在单线程内使用才完全安全,见 src/gevent/threadpool.py);
  • src/gevent/thread.py:BoundedSemaphore被用作线程内互斥原语。

3. 与事件/结果对象同族

Semaphore继承自AbstractLinkable(src/gevent/_abstract_linkable.py),与Event、AsyncResult共享“可链接(linkable)”基础设施:rawlink(callback)注册就绪回调、unlink(callback)撤销(src/gevent/_abstract_linkable.py),_check_and_notify在就绪且有链接时调度通知(src/gevent/_abstract_linkable.py)。这也解释了为何Semaphore可以直接传给gevent.wait([s])等待其就绪——测试 src/gevent/tests/test__semaphore.py 验证了这一点(对应 issue #1287)。

快速决策指南

你的需求选择
只做互斥,单 greenlet 内不会重入Semaphore(1)
互斥且可能递归进入临界区RLock
限制并发数量 NSemaphore(N)
限制并发数量且想尽早暴露“释放过多”的 bugBoundedSemaphore(N)(官方推荐默认选项)
资源无上限 / 对象本身线程安全,只是不想改代码DummySemaphore

导入规范提醒:一律from gevent.lock import Semaphore, BoundedSemaphore, DummySemaphore, RLock,不要从gevent._semaphore导入;否则在纯 Python/PyPy 环境下会绕过_AtomicSemaphore包装,得不到与 Cython 编译版本一致的原子性保证。

版本要点速查

  • 1.1a1:DummySemaphore.acquire始终返回True;
  • 1.1rc3:DummySemaphore接受并忽略value参数;
  • 1.4.0:明确等待者唤醒顺序未指定;
  • 1.5a3:等待者按等待顺序(FIFO)唤醒;rawlink自动 unlink;RLock.acquire新增timeout;
  • 20.5.1:RLock新增hub参数;
  • 20.12.0:Semaphore 改进多线程支持,不再为不存在的线程 hub 强建实例;
  • 24.2.1:跨线程操作使用 Python 3 原生锁超时替代自旋;
  • 25.4.1:RLock.locked()新增。

如需继续深入,可对照阅读 src/gevent/lock.py、src/gevent/_semaphore.py、src/gevent/_abstract_linkable.py,以及两个测试文件 src/gevent/tests/test__semaphore.py 与 src/gevent/tests/test__lock.py 中的完整用例。

  • 后端

【免费下载链接】gevent

Coroutine-based concurrency library for Python

项目地址:https://gitcode.com/gh_mirrors/ge/gevent
点击查看免费下载

相关推荐

上一篇:终极B站视频批量下载指南:Bilidown让8K高清收藏变得简单
下一篇:Thorium浏览器:重新定义Chromium性能极限的终极优化方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 2:10:11

Wand-Enhancer:本地 3 分钟解锁 Pro

Wand-Enhancer&#xff1a;本地 3 分钟解锁 Pro 【免费下载链接】Wand-Enhancer Advanced UX and interoperability extension for Wand (WeMod) app 项目地址: https://gitcode.com/GitHub_Trending/we/Wand-Enhancer Wand-Enhancer 是一个开源工具&#xff0c;通过本地…

作者头像 李华
网站建设 2026/10/7 2:09:50

Eve REST API 自定义 ID 字段实战:为资源接入 UUID 唯一标识

后端Web框架 【免费下载链接】eve REST API framework designed for human beings 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ev/eve 点击查看 免费下载 Eve 默认以 MongoDB 的 ObjectId 作为文档唯一标识&#xff0c;但当业务集合使用 UUID 等自定义主键时&#…

作者头像 李华