两个线程都读取到同一任务是 queued,各自停在屏障前,等两者都到齐后继续写 running。两边的领取记录各有一次相同的 task-0001:列表写入本身即使没有损坏,**“检查状态 → 标记领取”**的复合业务动作也完成了两次。这是在00 固定的普通 CPython 3.12.3 环境下,由屏障确定时序得到的反例,不依赖盲目多跑几千次“碰巧复现”。

27 的同一任务输入仍是 "One two one"。本篇只把领取动作单独抽出来观察;故意错误的领取函数放在实验文件中,不接管正式 tasklab 执行入口。正式 SQLite 仓储仍从同一 app.run 计算词频,并按 21 的事务规则回写结果。

屏障放在检查与更新之间

反例里每个线程先通过 query(store, task_id) 看见 queued,再等 threading.Barrier(2),随后直接设置 running 并记录一次领取。屏障保证两边在任何一边更新之前都已经作出“可以领取”的判断。实验记录 claims == ["task-0001", "task-0001"];state 最后是 running,却没有词频结果,因为这条反例只做了领取。它不是已经成功执行的任务,也不能拿来证明数据库里出现双份结果。

安全对照把 Barrier 放在锁外,让两个线程都准备竞争;拿到 Lock 后才一起检查状态并写入 running。第一个成功,第二个读到已改的状态,不领取;结果 len(claims) == 1。如果把锁只套在单独的写入或只套在单独的读取上,两者之间仍有空隙,不能由“使用过 Lock”四个字自动推出不变量成立。

另一个对照是真实 SQLite 文件:两个线程竞争同一持久任务,第一位在业务钩子中等待 Event,第二位发出竞争请求,释放第一位后,事务让其中一次写入成功并提交,另一位看见任务已非 queued,抛 ValueError: only queued tasks can run。最终重开仓储查询只有一条 succeeded 结果 {"one": 2, "two": 1}。这里 SQLite 的 BEGIN IMMEDIATE 和状态重查共同形成提交约束;并不声称整个任务在等待期间可让其他写者不受阻塞。锁与数据库事务各保护各自范围,不能跨进程用一把仅在进程内的 threading.Lock 替代 SQLite 原子更新。

1
2
3
cd examples/python-language
PYTHONPATH=src /usr/bin/python3.12 experiments/28_claims.py
PYTHONPATH=src /usr/bin/python3.12 -m unittest discover -s tests -v

本次反例确实记录两次领取、锁对照一次领取、SQLite 对照一次提交与一次拒绝,累计 75 项测试通过。原始输出、正常构建信息与页面在 examples/python-language/evidence/28/20261007-gil/。常规 CPython 3.12.3 的 Py_GIL_DISABLED 未设置;00 已记录当时 GIL 探针在持有 GIL 的线程得到 1,sys._is_gil_enabled 在该版本不可用。该信息说明这次环境,不是对所有 Python 实现或扩展模块的断言。

两种不同的“并行”问题

GIL 约束普通 CPython 中 Python 字节码的执行,但不替业务把多步状态迁移包成事务。这个实验关心正确性,不是比较吞吐;即使所有线程永远不在同一时刻执行 Python 代码,切换仍可能恰好发生在读取和更新之间。第 29 篇讨论线程池的排队、取消和 shutdown,30 才对照进程成本;38 才核对 free-threaded 构建、实际 GIL 开关与扩展兼容。当前没有 free-threaded 解释器,不能拿此处普通构建的结果替代 38 的另一构建实验。

预测题

两个线程都已经读取到 queued,现在仅把 claims.append(task_id) 放在锁里,但状态读取与更新仍在锁外,能保证只领取一次吗?

答案:不能。两个线程已经独立完成了是否可领取的判断,锁只让两次追加先后发生,无法撤销其中一人的判断;仍可能出现两条同一 ID 的领取记录。必须把检查和改变置于同一锁保护区,或让数据库事务作原子性约束。本章锁对照实际检验了前者,SQLite 对照检验了后者。

变更题

将屏障移到两个线程开始之前,而在状态读取与设置之间不再同步。运行一次只看到一个领取,是否能证明没有竞态?

答案:不能。此时调度可能让一个线程完成检查和设置,另一线程才读取;一次无冲突是允许的时序,但不能否定另一个重复领取的时序。把 Barrier 放在两次检查之后,再断言错误行为出现;修复后把同一个起点屏障放在锁外,并断言领取一次。测试安全性不能依赖“很难撞到”。

29 将用有界线程工作者和 Future 区分取消接受与工作是否真的停止。系列导航只列已生成的文章。

参考资料