《代码整洁之道》—第13章13.9节测试线程代码

  1. 云栖社区>
  2. 博客>
  3. 正文

《代码整洁之道》—第13章13.9节测试线程代码

异步社区 2017-05-02 10:22:00 浏览954
展开阅读全文

本节书摘来自异步社区《代码整洁之道》一书中的第13章13.9节测试线程代码,作者【美】Robert C. Martin,更多章节内容可以访问云栖社区“异步社区”公众号查看。

13.9 测试线程代码
证明代码的正确性不切实际。测试并不能确保正确性。然而,好的测试却能尽量降低风险。这对于所有单线程解决方案都是对的。当有两个或多个线程使用同一代码段和共享数据,事情就变得非常复杂了。

建议:编写有潜力曝露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,跟踪错误。别因为后来测试通过了后来的运行就忽略失败。

有一大堆问题要考虑。下面是一些精练的建议:

将伪失败看作可能的线程问题;
先使非线程代码可工作;
编写可插拔的线程代码;
编写可调整的线程代码;
运行多于处理器数量的线程;
在不同平台上运行;
调整代码并强迫错误发生。

13.9.1 将伪失败看作可能的线程问题
线程代码导致“不可能失败的”失败。多数开发者缺乏有关线程如何与其他代码(可能由其他作者编写)互动的直觉。线程代码中的缺陷可能在一千或一百万次执行中才会显现一次。重复执行想要复现问题令人沮丧。所以开发者常常会将失败归咎于宇宙射线、硬件错误或其他“偶发事件”。最好假设这种偶发事件根本不存在。“偶发事件”被忽略得越久,代码就越有可能搭建于不完善的基础之上。

建议:不要将系统错误归咎于偶发事件。

13.9.2 先使非线程代码可工作
这看起来太浅显,但强调一下不无益处。确保线程之外的代码可工作。通常,这意味着创建由线程调用的POJO。POJO与线程无涉,所以可在线程环境之外测试。能放进POJO中的代码越多越好。

建议:不要同时追踪非线程缺陷和线程缺陷。确保代码在线程之外可工作。

13.9.3 编写可插拔的线程代码
编写可在数个配置环境下运行的线程代码:

单线程与多个线程在执行时不同的情况;
线程代码与实物或测试替身互动;
用运行快速、缓慢和有变动的测试替身执行;
将测试配置为能运行一定数量的迭代。
建议:编写可插拔的线程代码,这样就能在不同的配置环境下运行。

13.9.4 编写可调整的线程代码
要获得良好的线程平衡,常常需要试错。一开始,在不同的配置环境下监测系统性能。要允许线程数量可调整。在系统运行时允许线程发生变动。允许线程依据吞吐量和系统使用率自我调整。

13.9.5 运行多于处理器数量的线程
系统在切换任务时会发生一些事。为了促使任务交换的发生,运行多于处理器或处理器核心数量的线程。任务交换越频繁,越有可能找到错过临界区或导致死锁的代码。

13.9.6 在不同平台上运行
2007年,我们做了一套关于并发编程的课程。该课程主要在OS X下开发,在运行于虚拟机的Windows XP上展示。用于演示的测试失败条件,在OS X上要比在XP上失败得更频繁。

被测试的代码已知是不正确的。这正强调了不同操作系统有着不同线程策略的事实,不同的线程策略影响了代码的执行。在不同环境中,多线程代码的行为也不一样[16]。应该在所有可能部署的环境中运行测试。

建议:尽早并经常地在所有目标平台上运行线程代码。

13.9.7 装置试错代码
并发代码中藏有缺陷,这并不罕见。简单的测试往往无法曝露这些缺陷。实际上,缺陷经常隐藏于一般处理过程中。可能好几个小时、好几天甚至好几个星期才会跳出来一次!

线程中的缺陷之所以如此不频繁、偶发、难以重现,是因为在几千个穿过脆弱区域的可能路径当中,只有少数路径会真的导致失败。经过会导致失败的路径的可能性惊人地低。所以,侦测与调试也非常之难。

怎么才能增加捕捉住如此罕见之物的机会?可以装置代码,增加对Object.wait( )、Object.sleep( )、Object.yield( )和Object.priority( )等方法的调用,改变代码执行顺序。

这些方法都会影响执行顺序,从而增加了侦测到缺陷的可能性。有问题的代码,最好尽早、尽可能多地通不过测试。

有两种装置代码的方法:

硬编码;
自动化。
13.9.8 硬编码
你可以手工向代码中插入wait( )、sleep( )、yield( )和priority( )的调用。在测试某段棘手的代码时,正当如此操作。

下面是个例子:

public synchronized String nextUrlOrNull() {
  if(hasNext()) {
    String url = urlGenerator.next();
    Thread.yield(); // inserted for testing.
    updateHasNext();
    return url;
  } 
  return null;
}

插入对yield( )的调用,将改变代码的执行路径,由此而可能导致代码在以前未失败过的地方失败。如果代码的确出错,那并非是因为你插入了yield( )方法调用[17]。代码出错了,这便是失败的原因。

这种手法有许多毛病:

你得手工找到合适的地方来插入方法调用;
你怎么知道在哪里插入调用、插入什么调用?
不必要地在产品环境中留下这类代码,将拖慢代码执行速度;
这是种无的放矢的手段。你可能找不到缺陷。实际上,这不在你把握之中。
我们所需要的,是一种在测试中但不在生产中实现的手段。我们还需要为多次运行轻易地调整配置,从而增加总的发现错误机会。

无疑,如果将系统分解为对线程及控制线程的类一无所知的POJO,就能更容易地找到装置代码的位置。而且,还能创建许多个以不同方式调用sleep、yield等方法的POJO测试。

13.9.9 自动化
可以使用Aspect-Oriented Framework、CGLIB或ASM之类工具通过编程来装置代码。例如,可以使用有单个方法的类:

public class ThreadJigglePoint {
  public static void jiggle() {
  }
}

可以在代码的不同位置调用这个方法:

public synchronized String nextUrlOrNull() {
  if(hasNext()) {
       ThreadJiglePoint.jiggle();
       String url = urlGenerator.next();
       ThreadJiglePoint.jiggle();
       updateHasNext();
       ThreadJiglePoint.jiggle();
       return url;
  } 
  return null;
}

如此,你就得到了一个随机选择无所作为、睡眠或让步的方面。

或者,想象ThreadJigglePoint类有两种实现。第一种实现jiggle什么都不做,在生产环境中使用。第二种实现生成一个随机数,在睡眠、让步或径直执行间做选择。如果上千次地做这种随机测试,大概就能找到一些缺陷的根源。假如测试都通过了,至少你可以说自己已谨慎对待。这种方法看似有点过于简单,但确是替代复杂工具的一种可选方案。

有一种叫做ConTest[18]的工具,由IBM开发,能做类似的事情,但做法却稍微复杂些。

要点是让代码“异动”,从而使线程以不同次序执行。编写良好的测试与“异动”相组合,能有效地增加发现错误的机会。

建议:使用异动策略搜出错误。

网友评论

登录后评论
0/500
评论