更新题目 Fire Event:多线程回调
题目 everpure-fire-event · 修改(rev 31) · · 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. 不用锁怎么做