竞态条件总是很有趣. 有其当它出现在类似Emacs这样的单线程程序中时就更显诡异了. 不过我已经遇到这个问题两次了.
当Emacs从process(译者注:由于Emacs中的process即可以是进程,也可以是一个网络链接,因此还是保持process不翻译)中读取数据时,会执行相应的filter函数. Emacs调用这个filter函数处理获得的数据后,再进行其他的工作. 然而在某些情况下,可能会发生在某个filter函数正在运行时运行另一个filter函数的情况. 触发的条件貌似是随机的.
Emacs只要一从process中读取到数据就会运行filter函数,即时这时候正在运行另一个filter函数. 很多情况下都会造成Emacs从process中读取数据. 即时你知道所有能触发Emacs从process中读取数据的函数也没什么用, 因为在大多数时候你无法控制,甚至无法知道Emacs运行的代码有哪些. 比如, 当你创建某个mode的buffer时,就会触发该mode的hook,而这个hook可能运行几乎任何代码.
我首次遇到这个问题是在使用 Circe 时发现的, 我用它作为Emacs上的IRC客户端. 在一个filter函数中,它会创建一个某个特定mode下的buffer. 而该mode可能会激活flyspell(Emacs的拼写检查工具). flyspell又会运行一个新来进行拼写检查. 运行新进程又会导致Emacs从process中读取数据,运行filter函数. 这个问题很难调试,因为只有当用户刚好在极短的事件内连续收到两条消息时才会触发.
你可以在Circe代码中看 我对该问题的说明
然后我又在 Elpy 中遇到了 相同的问题, 它是我的Python开发环境.
让我没想到的是, Emacs的 process-send-string
不仅仅只是发送数据,它还会同时从process中读取并处理数据.
这个问题出现的场景是,正在运行一个初始化函数,然后设置一个标志来标示初始化过程已经做过了,没有必要再进行以此初始化了.
然而该初始化函数会王process发送数据. 而该process的filter函数由会重新调用该初始化函数.
该初始化函数发现尚未初始化后又会向process发送数据,而这又会触发从process读取数据的过程,然后又一次触发初始化函数,再发送数据…
如此死循环下去最后栈溢出了.
你问怎么办? 只能使用一个动态作用域的”锁”了 (当然不是真的缩,它并不会阻塞程序的运行,不过也差不多吧).
(defvar my-lock nil)
(when (not my-lock)
(let ((my-lock t))
...))
对于像Emacs这样的单线程应用, 这样搞看起来没什么用,但事实证明它的能起作用.