面试准备

最近改动

更新题目 Fire Event:多线程回调

题目 everpure-fire-event · 修改(rev 31) · cc · 2026-10-25 19:33

body

@@ -16,5 +16,5 @@
   - 回调之间可能同时执行(线程 F 上的旧回调和当场执行的新回调),默认写进接口约定。必须一个一个执行,就全部交给一个工作线程,积压的回调在锁里提交。
   - 并发执行回调只在回调慢、在等 I/O 时划算。更好的设计是让登记的人传执行器。
-  - 可重置用代数计数器。
+  - 可重置用一个 fire 次数计数器(generation),等的是开始等之后的第一次 fire。
   - 无锁用 CAS,把标志和链表合成一个原子的头指针。
   - 可选知识:C++ 里锁外读写的标志要 `std::atomic`,`volatile` 不够。
@@ -838,5 +838,5 @@
 Trigger 版本有个坑。如果 `wait()` 还是写成 `while not self._fired: self._cond.wait()`,那么 fire 之后紧接着 reset,被叫醒的线程拿到锁时看到的已经是 False,它会以为是虚假唤醒,又睡回去,这次 fire 就被它错过了。我实测这种写法,等待线程确实一直睡着。
 
-解决办法是让等待者等「我开始等之后,发生过至少一次 fire」这个条件。用一个代数计数器(generation)记录 fire 的次数:
+解决办法是让等待者等「开始等之后的第一次 fire」。用一个计数器 `_generation` 记下一共 fire 过几次(generation 原意是「第几代」):
 
 ```python
@@ -845,13 +845,11 @@
         self._cond = threading.Condition()
         self._fired = False
-        self._generation = 0                # 每 fire 一次加 1
+        self._generation = 0                # fire 一次加 1,记的是一共 fire 过几次
 
     def wait(self):
         with self._cond:
-            if self._fired:
-                return
-            gen = self._generation
-            while self._generation == gen:  # 等的是「这之后 fire 过」
-                self._cond.wait()
+            gen = self._generation          # 记下开始等的时候 fire 过几次
+            while self._generation == gen and not self._fired:
+                self._cond.wait()           # 等到这之后的第一次 fire
 
     def fire(self):
@@ -866,5 +864,13 @@
 ```
 
-同样「fire 后立刻 reset」的实测里,这个版本的等待线程正常醒来了。
+循环条件有两半。`self._generation == gen` 的意思是「开始等之后还没有 fire 过」:只要 fire 过一次,计数就变了,哪怕紧接着又 reset,等待者醒来一看计数变了,也会返回。`not self._fired` 只在刚进来时起作用:已经 fire 过、还没 reset,就不用等,直接返回。等的过程中每次 fire 都会让计数加 1,所以醒来靠的是前半个条件。
+
+实测(2026-10-25):
+
+- 已经 fire 过、没 reset,再来 `wait()`:立刻返回。
+- 先 `wait()`,再 fire:醒来。
+- 先 `wait()`,fire 后立刻 reset:醒来。只看标志的写法在这里一直睡着。
+- 先 `wait()`,只 reset 不 fire,或者只 `notify_all()` 不改状态:继续等,这是对的。
+- fire 并 reset 之后才来 `wait()`:继续等,等的是下一次 fire。
 
 ### 6. 不用锁怎么做