有时收到报告说程序偶尔会出现奇怪的错误,可一打开调试器想查找原因,错误却像变魔术一样消失了。只是加了一行用于检查的输出,问题就停止了;删掉那一行,问题又会重新出现。开发者们给这种一被查看就躲起来的缺陷起了一个借用物理学家名字的绰号,那就是“海森堡缺陷(Heisenbug)”。
简单来说,海森堡缺陷是指一旦试图调查原因就会消失或改变行为的软件缺陷。这个词借用了“观察行为本身会改变对象状态”的观察者效应,用来指那些特别难以重现和追踪的缺陷。
本文将依次介绍海森堡缺陷这一名称从何而来、缺陷为何会躲起来、与之并用的缺陷别名,以及捕捉海森堡缺陷的方法。
一看就躲的缺陷:海森堡缺陷
海森堡缺陷这一名称的由来

这个绰号诞生于物理学的著名原理与程序员的玩笑相遇之处。
(1) 借用物理学家名字的文字游戏
海森堡缺陷是一个文字游戏,把提出不确定性原理的德国物理学家维尔纳·海森堡(Werner Heisenberg)的名字与意为虫子的“bug”叠加在一起。其用意是让人联想到观察者效应,即在试图观察的那一刻对象就会发生变化。
(2) 最早见于记录的时间
已知最早的记录是1983年美国计算机协会(ACM)的出版物。1985年,数据库研究者吉姆·格雷(Jim Gray)在一篇讨论软件故障的论文中使用了这个词,因此也曾被误认为是他创造的。
(3) 电子工程里的老邻居
在电子工程领域,早就有一个称呼类似现象的词,叫作探针效应(probe effect)。它指的是仅仅把测量用的探针接到电路上,设备的运行就会发生变化的现象。
不过,这个名称也受到批评。有人指出,不确定性原理和观察者效应是两个不同的概念,而这个绰号却把二者混为一谈。因此,与其把海森堡缺陷看作严谨的科学术语,不如把它当作开发一线的一句机智行话。
海森堡缺陷为什么会躲起来

缺陷并不是真的会看人脸色。而是用来查找原因的工具和方法,会一点点改变程序的运行条件。
(1) 执行时间发生变化
用调试器逐行执行时,程序会比平时慢得多。在多个任务同时运行的程序中,只要任务顺序稍有错位就会产生的竞态条件(race condition)十分常见,而速度一变,这种错位就可能不再发生。
(2) 内存布局发生变化
插入用于检查的输出语句,或为了调试而重新构建,都会改变变量在内存中的位置。如果原因是未初始化的变量或指向错误位置的指针,那么位置一变,症状也会随之改变。
(3) 是否优化发生变化
常有这样的情况:只有为性能而优化过的程序会出错,而为调试关闭优化的程序却一切正常。例如,计算值保存在寄存器中和移到内存中时精度不同,浮点数比较的结果就可能改变。
归根结底,海森堡缺陷是一个真实存在的缺陷,随着运行条件的不同时而显露、时而隐藏。关键在于,症状消失了并不等于原因也消失了。

与海森堡缺陷并用的缺陷别名

随着海森堡缺陷一词广泛流行,又出现了多个借用科学家名字的缺陷别名。
- 玻尔缺陷(Bohrbug):像物理学家尼尔斯·玻尔的原子模型一样遵循固定规则,在相同条件下总是以相同方式出现的缺陷。由于容易重现,相对容易捕捉。
- 曼德缺陷(Mandelbug):原因过于复杂,以致行为看起来混乱无序的缺陷。名称来自研究分形的数学家伯努瓦·曼德博。
- 薛定谔缺陷(Schrödinbug):在阅读代码的开发者发现这段代码本来就不可能正常运行之后,才真正开始引发问题的缺陷。
- 希格斯缺陷子(Higgs-bugson):从日志和用户报告来看似乎确实存在,但在开发环境中却难以重现的缺陷。
这些别名大多源于玩笑,但用来简短区分缺陷能否重现、原因有多复杂,还是很有用的。
捕捉海森堡缺陷的方法

海森堡缺陷虽然棘手,但只要换一种思路,完全可以抓住它。
(1) 不中断执行,留下记录
不要用调试器让程序停下来,而是留下对执行流程干扰较小的轻量记录,在问题发生后再进行分析。减少观察带来的影响是第一原则。
(2) 原样重现相同条件
尽可能让构建设置、输入值和运行环境与出问题时完全一致。也可以使用能把执行过程录制下来、再原样回放的工具。
(3) 缩小原因范围
从未初始化的变量、错误的指针、任务之间的顺序问题等常引发海森堡缺陷的原因开始,逐一排查。代码检查工具和内存检查工具能大大减轻这一过程的负担。
(4) 不因消失就结束
如果改了一行代码后症状停止了,就要弄清缺陷是被修复了,还是只是暂时躲了起来。只有能解释清楚原因时,才能说问题已经解决。
海森堡缺陷提醒开发者,就连用于观察的工具也是系统的一部分。即使在看不到缺陷的时候,也需要追问运行条件究竟改变了什么。
不满足于症状消失的那一刻,而是把原因一直追查到底,这正是开发可靠软件的第一步。