蚊章汇 - Heisenbugs
type: channel
link: https://linuxcn.bwsl.wang/tag/Heisenbugs/
items: 编程的乐趣：快速终止！

编程的乐趣：快速终止！
type: item
date: 2014-07-29T01:37:00+08:00
description: 当软件出现问题的时候，它应该以一种很容易引起注意的方式马上终止。这种“快速终止”的方式值得借鉴，我们会在这期专栏里谈谈这个重要的概念。一开始，“快速终止”看上去是一种会影响可靠性的不好的实践——为什么一个系统在还可以继续运行的时候要崩溃（或者说终止）？对于这个，我们需要理解，快速终止是和Heisenbugs（对于不易复现bug的一种称呼）紧密联系在一起的。考虑一下Bohrbugs（对于能够重现的bug的一种称呼），它们在一个给定输入的条件下总是会出现，比如，访问空指针。这类问题很容易测试、复现并修复。而如今，所有有经验的程序员应该都面对过这样的情形：导致崩溃的bug在重启软件后就不再出现了。不管花多少时间或努力去重现问题，那个bug就是跟我们捉迷藏。这种bug被称为Heisenbugs。花在寻找、修复和测试Heisenbugs上的努力比起Bohrbugs来说，要高出一个数量级。一种避免Heisenbugs的策略是将它们转化为Bohrbugs。怎么做呢？预测可能导致Heisenbugs的因素，然后尝试将它们变成Bohrbugs。是的，这并不简单，而且也并不是一定可行，但是让我们来看一个能产生效果的特殊例子。并发编程是Heisenbugs经常出现的一个典范。我们的例子就是一个Java里和并发相关的问题。在遍历一个Java集合的时候，一般要求只能通过Iterator的方法对集合进行操作，比如remove()方法。而在遍历期间，如果有另一个线程尝试修改底层集合（因为编程时留下的错误），那么底层集合就可能会被破坏（例如，导致不正确的状态）。类似这种不正确的状态会导致不确定的错误——假如我们幸运的话（实际上，这很不幸！），程序可以继续执行而不会崩溃，但是却给出错误的结果。这种bug很难重现和修复，因为这一类的程序错误都是不确定的。换句话说，这是个Heisenbug。幸运的是，Java Iterators会尝试侦测这种并发修改，并且当发现时，会抛出异常ConcurrentModificationException，而不是等到最后再出错——那样也是没有任何迹象的。换句话说，Java Iterators也遵从了“快速终止”的方法。如果一个ConcurrentModificationException异常在正式版软件中发生了呢？根据在Javadoc里对这个异常的说明，它“只应该被用于侦测bug”。换句话说，ConcurrentModificationException只应该在开发阶段监听和修复，而不应该泄漏到正式代码中。好吧，如果正式软件确实发生了这个异常，那它当然是软件中的bug，应当报告给开发者并修复。至少，我们能够知道曾经发生过一次针对底层数据结构的并发修改尝试，而这是软件出错的原因（而不是让软件产生错误的结果，或是以其他现象延后出错，这样就很难跟踪到根本原因）。“防止崩溃”的途径就意味着开发健壮的代码。一个很好的编写容错代码的例子就是使用断言。很可惜的是，关于断言的使用有大量不必要的公开争论。其中主要的批评点是：它在开发版本中使用，而在发布版中却被关掉的。不管怎么样，这个批评是错误的：从来没有说要用断言来替代应该放到发布版软件中的防御式检查代码。例如，断言不应该用来检查传递给函数的参数是否为空。相应的，应该用一个if语句来检查这个参数是否正确，否则的话抛出一个异常，或是提前返回，来适合上下文。然而，断言一般可以用于额外检查代码中所作出的假设，这些假设应该一直为真才正常。例如，用一个语句来检查在进行了入栈操作后，栈应该不是空的（例如，对“不变量”的检查）。所以，快速终止，随时中断，那么你就走在开发更加健壮代码的道路上了。