pytest-xdist 默认以独立进程运行 worker,threading.lock 和 multiprocessing.lock 均无效;应使用文件锁(flock)或 pytest-xdist 的 --dist=loadgroup 分组机制实现跨进程同步。

pytest-xdist 默认不共享进程间状态
你写的 threading.Lock 或 multiprocessing.Lock 在 pytest-xdist 下基本没用——因为默认每个 worker 是独立子进程,内存不共享,锁对象各自实例化,形同虚设。
常见错误现象:FileExistsError 写临时文件时撞车、sqlite3.OperationalError: database is locked、或者测试逻辑里靠全局变量计数/标记状态却始终为初始值。
- pytest-xdist 的
-n启动的是多个独立 Python 进程(不是线程),threading模块完全无效 -
multiprocessing.Lock仅在 fork 模式下可能复用,但 xdist 默认用 spawn(尤其 Windows/macOS),锁无法继承或传递 - 真正能跨 worker 共享的只有外部资源:文件、数据库、Redis、本地 socket 或文件锁(
flock)
用文件锁(flock)做轻量级跨进程互斥
不需要引入 Redis 或启动额外服务,一个本地文件 + flock 就能实现可靠加锁,适合控制测试中“只允许一个 worker 执行某段逻辑”这类场景。
使用场景:初始化一次数据库 schema、生成一份共享 fixture 数据、清理某个临时目录、调用一次外部 mock server 启停。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
- Python 标准库
fcntl.flock只支持 Unix/Linux/macOS;Windows 需用msvcrt.locking或改用portalocker库 - 锁文件路径必须所有 worker 进程可读写,推荐放在项目根目录下明确路径,比如
./.test-lock - 务必在
finally块中释放锁,否则一个 worker 挂掉会导致锁永久占用
import fcntl
import os
def acquire_global_lock(lock_path="./.test-lock"):
fd = os.open(lock_path, os.O_CREAT | os.O_RDWR)
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
return fd
except (OSError, IOError):
os.close(fd)
raise RuntimeError(f"Failed to acquire lock on {lock_path}")
def release_global_lock(fd):
fcntl.flock(fd, fcntl.LOCK_UN)
os.close(fd)
pytest fixture 中安全封装共享锁逻辑
把锁操作包装成 session-scoped fixture,让需要串行执行的测试自动排队,避免在每个 test 函数里手写锁逻辑出错。
参数差异:注意 scope="session" 是跨 worker 共享的关键,而 autouse=True 容易误触发——只应在明确需要的测试类或模块上显式 use。
- fixture 返回的是锁的持有状态(如 fd),不是锁本身;释放必须在 fixture teardown 阶段完成
- 不能依赖
yield自动 cleanup:worker 进程退出不一定走 yield 后逻辑,必须用request.addfinalizer - 如果锁获取失败(比如超时),建议抛出
pytest.skip()而不是raise,避免整个 session 失败
import pytest
import fcntl
import os
@pytest.fixture(scope="session")
def global_init_lock(request):
lock_fd = None
lock_path = "./.test-init-lock"
try:
lock_fd = os.open(lock_path, os.O_CREAT | os.O_RDWR)
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except (OSError, IOError):
pytest.skip("Another worker holds the init lock")
def _release():
if lock_fd is not None:
try:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
finally:
os.close(lock_fd)
request.addfinalizer(_release)
return lock_fd
替代方案:用 pytest-xdist 的 --dist=loadgroup + 分组标签更可控
如果你的“并发加锁”本质是想让某些测试不并行跑(比如都操作同一套 DB),其实没必要硬搞跨进程锁——直接用 xdist 内置分组机制更稳。
性能影响:flock 在高并发测试下会有排队延迟;而 --dist=loadgroup 由 pytest-xdist 自己调度,无 IO 等待,响应更快。
- 给需要串行的测试打标签:
@pytest.mark.serial - 运行时指定:
pytest -n 4 --dist=loadgroup --group-by=mark - 它会把所有
serial测试归到同一个 worker,其他测试照常并发,无需任何锁代码 - 注意:这个策略要求你主动分类,不适合“动态决定是否加锁”的场景(比如根据环境变量判断)
threading.Lock,却没检查 ps aux | grep pytest 看看到底起了几个进程。Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










