更新题目 Fire Event:多线程回调
题目 everpure-fire-event · 修改(rev 33) · · 2026-10-25 19:54
body
@@ -693,4 +693,19 @@ 实测:old 用 `threading.Event` 等信号,设了 1 秒超时。old 一直等到超时也没等到信号,线程 R 的 `register` 卡了 999 毫秒,等 old 超时返回才拿到锁。不设超时就是永远卡住。回调是别人写的,事件类没法知道回调会不会去等别的线程。 +一个常见的具体形式是:回调自己新开一个线程 T,让 T 去调 `register(x)`。T 看到已经触发,要先拿到 `_run_lock` 才能执行 x,而这把锁正被线程 F 拿着。`RLock` 只对同一个线程可重入,T 是另一个线程,只能等。接下来要看回调等不等 T: + +- 回调不等 T:回调很快结束,F 执行完这批回调后放掉 `_run_lock`,T 拿到锁执行 x。不会卡住,只是 x 排到了这批回调后面。 +- 回调等 T,比如调了 `t.join()`:F 在等 T 结束,T 在等 F 放锁,就是上表的情形,两边互相等住。 + +实测(2026-10-25,类从本页原样抽出来跑,回调等 T 时最多等 1 秒): + +| 写法 | 回调不等 T | 回调等 T | +| --- | --- | --- | +| 做法一 | 不卡,x 在回调结束后执行 | 卡住:等满 1 秒,T 还停在 `register` 里,超时以后回调结束、放锁,x 才执行 | +| 做法二 | 不卡 | 不卡:T 的 `register` 只是把 x 交给工作线程就返回 | +| 「代码」一节的写法 | 不卡,x 在线程 T 上当场执行 | 不卡 | + +「代码」一节的写法执行回调时什么都不占,所以回调怎么等都不会卡。做法二有一个类似的坑,见下面做法二的代价。 + 另外,这个做法只保证不重叠,不保证顺序。`fire` 放掉 `_lock` 之后、拿到 `_run_lock` 之前有一个空档,线程 R 可能在这个空档里先拿到 `_run_lock`。实测在这两步之间停 30 毫秒,完成顺序是 new、old。 @@ -756,4 +771,5 @@ - **工作线程要有人关。** `close()` 里的 `shutdown(wait=True)` 等已经提交的回调执行完,再结束工作线程。关了以后再 `submit` 会抛 `RuntimeError`,已经排在队列里的回调去调 `register` 也一样。实测 `fire` 之后马上 `close()`,排在后面的那个回调在里面登记的新回调没有执行,日志里记的是 `cannot schedule new futures after shutdown`。所以要在确定不会再有人登记以后再关。 - **所有回调挤在一个线程上。** 一个慢回调会推迟后面所有回调。 +- **回调不能等它登记的新回调执行完。** 新回调排在同一个工作线程的队列里,要等当前回调执行完才轮得到。当前回调如果去等它执行完,就成了自己等自己。实测回调新开线程 T 登记 x,再等 x 执行完:只等 T 登记完没问题(上面表里做法二那一格),等 x 执行完则等满 1 秒,x 都没执行。 asyncio 用的就是这个模型:所有回调都在事件循环(event loop)所在的那一个线程上依次执行。`asyncio.Future` 已经完成时登记的回调也一样,会先用 `loop.call_soon` 排进事件循环,轮到了再执行([文档](https://docs.python.org/3/library/asyncio-future.html))。