- 后端
【免费下载链接】gevent
Coroutine-based concurrency library for Python
导读
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 默认为 1BoundedSemaphore检查当前值不会超过初始值:若过度释放(当前值已达到初始值仍调用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 参数以兼容 SemaphoreDummySemaphore拥有与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):
- 资源确实有限(如固定大小的线程池)→ 用真正的
Semaphore;资源无上限 → 用DummySemaphore,从而支持代码完全不变; - 底层对象已知线程安全、无需互斥 → 用
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 可重复进入 passRLock是“同一 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 |
| 限制并发数量 N | Semaphore(N) |
| 限制并发数量且想尽早暴露“释放过多”的 bug | BoundedSemaphore(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
相关推荐
Podman 仓库中的 modern-go/concurrent 并发原语解析:Map 与 Executor 的源码级指南
Podman 仓库中的 modern go/concurrent 并发原语解析:Map 与 Executor 的源码级指南 本文基于 Podman 仓库 ven
容器运行时云原生CLIlinux-insides 源码解析:Linux 内核信号量(Semaphore)同步原语完全指南
linux insides 源码解析:Linux 内核信号量(Semaphore)同步原语完全指南 本篇是开源书籍《linux insides》 同步原语章节
文档教程操作系统CPython asyncio 同步原语全解析:Lock、Event、Condition、Semaphore 与 Barrier 的用法与源码实现
CPython asyncio 同步原语全解析:Lock、Event、Condition、Semaphore 与 Barrier 的用法与源码实现 本指南以 C
编程语言语言运行时解释器标准库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考