我应该如何testing线程代码?

到目前为止,我已经避免了testingmultithreading代码的噩梦,因为它看起来像是一个雷区。 我想问一下,人们是如何去testing依赖线程来成功执行的代码的,或者人们是如何去testing那些只有当两个线程以一种给定的方式进行交互时才会出现的问题呢?

这对于今天的程序员来说似乎是一个非常关键的问题,将我们的知识集中在这个imho上是非常有用的。

看,有没有简单的方法来做到这一点。 我正在开发一个天生就是multithreading的项目。 事件来自操作系统,我必须同时处理它们。

处理testing复杂的multithreading应用程序代码的最简单的方法是:如果testing过于复杂,那么你做错了。 如果您有一个具有多个线程的单个实例,并且您无法testing这些线程是否遍布彼此的情况,那么您的devise需要重做。 它既简单又复杂。

编程multithreading有很多方法可以避免线程同时在实例中运行。 最简单的是使所有的对象不变。 当然,这通常是不可能的。 所以你必须在你的devise中确定线程与同一个实例交互的地方,并减less这些地方的数量。 通过这样做,您可以隔离几个实际发生multithreading的类,从而降低testing系统的总体复杂性。

但是你必须认识到,即使这样做,你仍然不能testing两个线程彼此相连的每一个情况。 要做到这一点,你必须在同一个testing中同时运行两个线程,然后在任何给定的时刻确切地控制它们正在执行的行。 你能做的最好的就是模拟这种情况。 但是这可能要求您专门为testing编写代码,而这只是朝着真正的解决scheme迈进了一大步。

testing线程问题的代码的最好方法是通过代码的静态分析。 如果您的线程代码不遵循有限的线程安全模式集,那么您可能会遇到问题。 我相信VS中的代码分析确实包含了线程的一些知识,但可能并不多。

按照目前的情况来看(可能会有一个好时机),testingmultithreading应用程序的最好方法是尽可能减less线程代码的复杂性。 最大限度地减less线程交互的区域,尽可能地进行testing,并使用代码分析来识别危险区域。

当这个问题发布的时候已经有一段时间了,但是还没有回答…

kleolb02的答案是一个很好的答案。 我会尝试进入更多的细节。

有一种方法,我为C#代码练习。 对于unit testing,您应该能够编程可重复的testing,这是multithreading代码中最大的挑战。 所以我的答案旨在强制asynchronous代码到同步工作的testing工具。

这是Gerard Meszardos的书“ xUnit Test Patterns ”的一个想法,被称为“Humble Object”(p.695):你必须将核心逻辑代码和任何闻起来像asynchronous代码的东西分开。 这将导致核心逻辑的类,它同步工作

这使您能够以同步的方式testing核心逻辑代码。 您可以绝对控制正在进行的核心逻辑调用的时间,从而可以进行可重复的testing。 这是分离核心逻辑和asynchronous逻辑的好处。

这个核心逻辑需要由另外一个类来包装,这个类负责asynchronous接收对核心逻辑的调用, 并将这些调用委托给核心逻辑。 生产代码将只能通过该类访问核心逻辑。 因为这个类只能委托调用,所以这是一个非常“愚蠢”的类,没有太多的逻辑。 所以你可以把这个asynchronous工作class的unit testing保持在最低限度。

以上任何东西(testing类之间的交互)都是组件testing。 同样在这种情况下,如果你坚持“谦虚对象”模式,你应该能够绝对控制时机。

确实很难! 在我的(C ++)unit testing中,我已经沿着所使用的并发模式分成了几类:

  1. unit testing在单个线程中运行,并且不是线程感知的类 – 简单,像往常一样testing。

  2. Monitor对象 (在调用者的控制线程中执行同步方法的那些对象)的unit testing公开一个同步的公共API – 实例化多个模拟线程,这些线程运行API。 构build行使被动对象内部条件的场景。 包括一个较长时间的运行testing,基本上可以在很长一段时间内从多个线程跳出来。 我知道这是不科学的,但确实build立了信心。

  3. 活动对象 (封装自己的线程或控制线程的那些)的unit testing – 类似于上面的#2,具体取决于类的devise。 公共API可能是阻塞的或非阻塞的,呼叫者可能获得期货,数据可能到达队列或需要出列。 这里有很多可能的组合。 白色的盒子。 仍然需要多个模拟线程来调用被测对象。

另外:

在我所做的内部开发人员培训中,我将“并发支柱”和这两种模式作为思考和分解并发问题的主要框架。 显然有更先进的概念,但是我发现这套基础知识可以帮助工程师摆脱困境。 如上所述,这也导致了更多单元可testing的代码。

近几年来,我在编写几个项目的线程处理代码时遇到了这个问题。 我提供了一个较迟的答案,因为大多数其他答案在提供替代scheme时并不真正回答有关testing的问题。 我的回答是针对无法替代multithreading代码的情况。 我确实涵盖了代码devise问题,还讨论了unit testing。

编写可testing的multithreading代码

首先要做的是将您的生产线程处理代码与所有执行实际数据处理的代码分开。 这样,数据处理可以作为单线程代码进行testing,multithreading代码所做的唯一事情就是协调线程。

第二件要记住的事情是multithreading代码中的错误是概率性的; 那些performance得最不经常的错误是潜入生产的错误,即使在生产中也难以复制,从而造成最大的问题。 出于这个原因,快速编写代码然后debugging它直到它工作的标准编码方法对于multithreading代码是一个坏主意; 它会导致代码容易的错误被修复,危险的错误仍然存​​在。

相反,在编写multithreading代码时,您必须以避免编写错误的态度编写代码。 如果你已经正确地删除了数据处理代码,那么线程处理代码应该足够小 – 最好是几行代码,最好是几十行代码 – 这样你就有机会编写代码,而不会写错误,当然也不会写很多的错误,如果你懂线程,慢慢来,小心点。

编写unit testingmultithreading代码

一旦multithreading代码被尽可能仔细地写入,那么为代码编写testing还是值得的。 testing的主要目的不是testing高度定时相关的竞争条件错误 – 不可能重复testing这样的竞争条件 – 而是testing您的locking策略以防止这样的错误允许多个线程按预期进行交互。

要正确testing正确的locking行为,testing必须启动多个线程。 为了使testing可重复,我们希望线程之间的交互以可预测的顺序进行。 我们不想在testing中从外部同步这些线程,因为这样可以屏蔽在线程不在外部同步的生产环境中可能发生的错误。 这就给线程同步留下了时间延迟,这是我必须编写multithreading代码testing时成功使用的技术。

如果延迟太短,则testing变得脆弱,因为较小的时间差异 – 例如在可能运行testing的不同机器之间 – 可能导致时序closures和testing失败。 我通常所做的就是从导致testing失败的延迟开始,增加延迟,使得testing在我的开发机器上可靠地传递,然后将延迟加倍,以便testing很有可能在其他机器上传递。 这确实意味着testing需要花费大量的时间,但是根据我的经验,仔细的testingdevise可以将时间限制在不超过十二秒。 由于您的应用程序中不应该有需要线程协调代码的地方,因此您的testing套件应该可以接受。

最后,跟踪testing中发现的错误数量。 如果你的testing有80%的代码覆盖率,那么可以预计你的错误率达到80%。 如果你的testingdevise得很好,但没有发现错误,那么有一个合理的机会,你没有额外的错误,只会出现在生产中。 如果testing抓住一两个错误,你可能仍然很幸运。 除此之外,您可能需要仔细审查甚至彻底重写您的线程处理代码,因为代码可能仍然包含隐藏的错误,直到代码生成时才会很难find,而且非常那么很难解决。

我也有严重的问题testingmultithreading代码。 然后,我在Gerard Meszaros的“xUnit Test Patterns”中find了一个非常酷的解决scheme。 他描述的模式被称为谦卑对象

基本上它描述了如何将逻辑提取到与环境分离的单独的,易于testing的组件中。 在你testing这个逻辑之后,你可以testing复杂的行为(multithreading,asynchronous执行等等)

有几个工具是相当不错的。 这里是一些Java的总结。

一些很好的静态分析工具包括FindBugs (提供了一些有用的提示), JLint , Java Pathfinder (JPF&JPF2)和Bogor 。

MultithreadedTC是一个很好的dynamic分析工具(集成到JUnit中),你必须build立你自己的testing用例。

IBM Research的ConTest很有趣。 它通过插入各种线程修改行为(例如,睡眠和收益)来随机地发现错误,从而testing你的代码。

SPIN是build模Java(和其他)组件的非常酷的工具,但是你需要有一些有用的框架。 这是很难使用的,但是如果你知道如何使用它,它是非常强大的。 很多工具在引擎盖下使用SPIN。

MultithreadedTC可能是最主stream的,但上面列出的一些静态分析工具绝对值得一看。

等待也可以帮助你写确定性的unit testing。 它允许您等待系统中某个状态更新。 例如:

await().untilCall( to(myService).myMethod(), greaterThan(3) ); 

要么

 await().atMost(5,SECONDS).until(fieldIn(myObject).ofType(int.class), equalTo(1)); 

它也有Scala和Groovy的支持。

 await until { something() > 4 } // Scala example 

我做了很多这个,是的,它很糟糕。

一些技巧:

  • GroboUtils用于运行多个testing线程
  • alphaWorks ConTest将仪器类别引起交错在迭代之间变化
  • 创build一个可throwable字段,并在tearDown检查它(请参见清单1)。 如果在另一个线程中发现了一个不好的exception,只需将其分配给throwable即可。
  • 我在清单2中创build了utils类,并发现它非常宝贵,尤其是waitForVerify和waitForCondition,这将大大提高testing的性能。
  • 在testing中充分利用AtomicBoolean 。 这是线程安全的,你经常需要一个最后的引用types来存储callback类等的值。 见清单3中的例子。
  • 确保总是给你的testing一个超时(例如@Test(timeout=60*1000) ),因为并发testing有时会在它们被破坏时永远挂起

清单1:

 @After public void tearDown() { if ( throwable != null ) throw throwable; } 

清单2:

 import static org.junit.Assert.fail; import java.io.File; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.Random; import org.apache.commons.collections.Closure; import org.apache.commons.collections.Predicate; import org.apache.commons.lang.time.StopWatch; import org.easymock.EasyMock; import org.easymock.classextension.internal.ClassExtensionHelper; import static org.easymock.classextension.EasyMock.*; import ca.digitalrapids.io.DRFileUtils; /** * Various utilities for testing */ public abstract class DRTestUtils { static private Random random = new Random(); /** Calls {@link #waitForCondition(Integer, Integer, Predicate, String)} with * default max wait and check period values. */ static public void waitForCondition(Predicate predicate, String errorMessage) throws Throwable { waitForCondition(null, null, predicate, errorMessage); } /** Blocks until a condition is true, throwing an {@link AssertionError} if * it does not become true during a given max time. * @param maxWait_ms max time to wait for true condition. Optional; defaults * to 30 * 1000 ms (30 seconds). * @param checkPeriod_ms period at which to try the condition. Optional; defaults * to 100 ms. * @param predicate the condition * @param errorMessage message use in the {@link AssertionError} * @throws Throwable on {@link AssertionError} or any other exception/error */ static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, Predicate predicate, String errorMessage) throws Throwable { waitForCondition(maxWait_ms, checkPeriod_ms, predicate, new Closure() { public void execute(Object errorMessage) { fail((String)errorMessage); } }, errorMessage); } /** Blocks until a condition is true, running a closure if * it does not become true during a given max time. * @param maxWait_ms max time to wait for true condition. Optional; defaults * to 30 * 1000 ms (30 seconds). * @param checkPeriod_ms period at which to try the condition. Optional; defaults * to 100 ms. * @param predicate the condition * @param closure closure to run * @param argument argument for closure * @throws Throwable on {@link AssertionError} or any other exception/error */ static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, Predicate predicate, Closure closure, Object argument) throws Throwable { if ( maxWait_ms == null ) maxWait_ms = 30 * 1000; if ( checkPeriod_ms == null ) checkPeriod_ms = 100; StopWatch stopWatch = new StopWatch(); stopWatch.start(); while ( !predicate.evaluate(null) ) { Thread.sleep(checkPeriod_ms); if ( stopWatch.getTime() > maxWait_ms ) { closure.execute(argument); } } } /** Calls {@link #waitForVerify(Integer, Object)} with <code>null</code> * for {@code maxWait_ms} */ static public void waitForVerify(Object easyMockProxy) throws Throwable { waitForVerify(null, easyMockProxy); } /** Repeatedly calls {@link EasyMock#verify(Object[])} until it succeeds, or a * max wait time has elapsed. * @param maxWait_ms Max wait time. <code>null</code> defaults to 30s. * @param easyMockProxy Proxy to call verify on * @throws Throwable */ static public void waitForVerify(Integer maxWait_ms, Object easyMockProxy) throws Throwable { if ( maxWait_ms == null ) maxWait_ms = 30 * 1000; StopWatch stopWatch = new StopWatch(); stopWatch.start(); for(;;) { try { verify(easyMockProxy); break; } catch (AssertionError e) { if ( stopWatch.getTime() > maxWait_ms ) throw e; Thread.sleep(100); } } } /** Returns a path to a directory in the temp dir with the name of the given * class. This is useful for temporary test files. * @param aClass test class for which to create dir * @return the path */ static public String getTestDirPathForTestClass(Object object) { String filename = object instanceof Class ? ((Class)object).getName() : object.getClass().getName(); return DRFileUtils.getTempDir() + File.separator + filename; } static public byte[] createRandomByteArray(int bytesLength) { byte[] sourceBytes = new byte[bytesLength]; random.nextBytes(sourceBytes); return sourceBytes; } /** Returns <code>true</code> if the given object is an EasyMock mock object */ static public boolean isEasyMockMock(Object object) { try { InvocationHandler invocationHandler = Proxy .getInvocationHandler(object); return invocationHandler.getClass().getName().contains("easymock"); } catch (IllegalArgumentException e) { return false; } } } 

清单3:

 @Test public void testSomething() { final AtomicBoolean called = new AtomicBoolean(false); subject.setCallback(new SomeCallback() { public void callback(Object arg) { // check arg here called.set(true); } }); subject.run(); assertTrue(called.get()); } 

如上所述,testingMT代码是否正确是一个相当困难的问题。 最后归结为确保代码中没有错误地同步数据竞争。 这样做的问题是线程执行(交错)的可能性是无限多的,在这种情况下,你没有太多的控制权(尽pipe如此,请阅读这篇文章)。 在简单的情况下,可以通过推理来certificate正确性,但通常情况并非如此。 特别是如果你想避免/最小化同步,而不是最明显的/最简单的同步选项。

我遵循的方法是编写高度并发的testing代码,以便可能发生未被发现的数据竞争。 然后我运行这些testing一段时间:)我曾经偶然发现一些计算机科学家在那里展示了一个这样的工具(从规范中随机地devisetesting,然后疯狂地运行它们,同时检查定义的不variables被打破)。

顺便说一句,我认为testingMT代码的这个方面在这里没有提到:确定你可以随机检查代码的不variables。 不幸的是,find这些不variables也是一个很难的问题。 另外,他们可能在执行过程中不能一直保持,所以你必须find/执行执行点,你可以期望他们是真实的。 把代码执行带到这样一个状态也是一个难题(而且本身可能会引发并发问题,哎呀,真是太难了!

一些有趣的链接阅读:

  • 确定性交错 :一个框架,允许强制某些线程交错,然后检查不variables
  • jMock Blitzer :压力testing同步
  • assertConcurrent :JUnit版本的压力testingsynronization
  • testing并发代码 :蛮力(压力testing)或确定性(去不变式)的两个主要方法的简短概述

另一种(有点)testing线程代码的方式,以及一般非常复杂的系统都是通过Fuzz Testing 。 这不是很好,它不会find所有的东西,但它可能是有用的,它很容易做到。

引用:

模糊testing或模糊testing是一种软件testing技术,为程序的input提供随机数据(“模糊”)。 如果程序失败(例如,通过崩溃或失败的内置代码断言),可以注意到缺陷。 模糊testing的优点在于testingdevise非常简单,并且对系统行为没有先入之见。

模糊testing通常用于采用黑盒testing的大型软件开发项目。 这些项目通常有预算来开发testing工具,而模糊testing是提供高成本比效益的技术之一。

然而,模糊testing并不能替代穷举testing或forms化方法:它只能提供系统行为的随机样本,在许多情况下,通过模糊testing只能certificate一个软件处理exception而不会崩溃,而不是行为正确。 因此,模糊testing只能被视为一种找虫工具,而不能保证质量。

Pete Goodliffe在线程代码的unit testing中有一系列的内容。

这个很难(硬。 我采取更简单的方法,并尝试保持从实际testing中抽象出的线程代码。 皮特确实提到我这样做的方式是错误的,但是我要么分手,要么我只是幸运。

对于Java,请查看JCIP的第12章。 有一些编写确定性的multithreadingunit testing的具体例子,以至lesstesting并发代码的正确性和不变性。

使用unit testing“certificate”线程安全是非常有趣的。 我的看法是,在各种平台/configuration上进行自动化集成testing会更好。

我以与处理任何unit testing相同的方式处理线程组件的unit testing,即控制和隔离框架的反转。 我在.Net领域发展起来,线程(除其他外)非常困难(我几乎不可能)完全隔离。

所以我写了一些类似这样的包装(简体):

 public interface IThread { void Start(); ... } public class ThreadWrapper : IThread { private readonly Thread _thread; public ThreadWrapper(ThreadStart threadStart) { _thread = new Thread(threadStart); } public Start() { _thread.Start(); } } public interface IThreadingManager { IThread CreateThread(ThreadStart threadStart); } public class ThreadingManager : IThreadingManager { public IThread CreateThread(ThreadStart threadStart) { return new ThreadWrapper(threadStart) } } 

从那里我可以轻松地将IThreadingManager注入到我的组件中,并使用我select的隔离框架使线程按照我预期的那样运行。

到目前为止,这对我来说非常有效,我使用了相同的方法来创build线程池,System.Environment,Sleep等等。

我喜欢写两个或更多的testing方法在并行线程上执行,并且每个testing方法都会调用被测对象。 我一直在使用Sleep()调用来协调来自不同线程的调用顺序,但这并不可靠。 这也慢了很多,因为你必须睡得足够长,时间通常是可以工作的。

我从编写FindBugs的同一组中find了multithreadingTC Java库 。 它可以让你指定事件的顺序,而不使用Sleep(),这是可靠的。 我还没有尝试过。

这种方法的最大局限是它只能让你testing你怀疑会造成麻烦的场景。 正如其他人所说,你真的需要将你的multithreading代码分离成less数几个简单的类,以便对它们进行彻底的testing。

一旦你仔细地testing了你所期望引起麻烦的场景,一个不合时宜的testing,在课堂上引发了一堆同时发生的请求,是寻找意外麻烦的好方法。

更新:我已经玩了multithreading的TC Java库,它运作良好。 我也将其一些function移植到我称为TickingTest的.NET版本。

看看我的相关答案

devise自定义障碍的testing类

它偏向于Java,但有一个合理的选项总结。

总之虽然(IMO)它不使用一些奇特的框架,将确保正确,但你怎么去devise你的multithreading代码。 拆分关注点(并发性和function性)对于提高信心是一个巨大的方法。 越来越多的面向对象的软件指导testing解释了一些比我更好的select。

静态分析和forms化方法(参见并发:状态模型和Java程序 )是一种select,但是我发现它们在商业开发中的使用有限。

不要忘记,任何负载/浸泡式的testing很难保证突出问题。

祝你好运!

我刚刚发现(对于Java)一个名为Threadsafe的工具。 它是一个静态分析工具,非常像findbugs,但特别指出了multithreading问题。 它不是testing的替代品,但我可以推荐它作为编写可靠的multithreadingJava的一部分。

它甚至会捕获一些非常微妙的潜在问题,如类包含,通过并发类访问不安全的对象,以及在使用双重检查的locking范例时发现丢失的volatile修饰符。

如果你写multithreading的Java 给它一个镜头。

以下文章提出了两个解决scheme。 包装一个信号量(CountDownLatch)并添加诸如从内部线程外部化数据的function。 实现这个目的的另一种方法是使用线程池(参见兴趣点)。

喷头 – 高级同步对象

上周我花了大部分时间在一家大学图书馆学习并发代码的debugging。 中心问题是并发代码是非确定性的。 通常情况下,学术debugging已经落入这三个阵营之一:

  1. 事件跟踪/重播。 这需要一个事件监视器,然后检查发送的事件。 在UT框架中,这将涉及手动发送事件作为testing的一部分,然后进行事后检查。
  2. 编写脚本。 这是您用一组触发器与运行代码交互的地方。 “在x> foo,baz()”。 这可以被解释为一个UT框架,在这个框架中你有一个运行时系统在特定的条件下触发给定的testing。
  3. 互动。 这在自动testing情况下显然不起作用。 ;)

现在,正如上面的评论者已经注意到的,你可以将你的并发系统devise成更确定的状态。 但是,如果你不这样做,你只是回到devise一个顺序系统了。

我的build议是专注于有一个非常严格的devise协议,什么是线程,什么不是线程。 如果你限制你的界面,使得元素之间的依赖性很小,那就容易多了。

祝你好运,并继续努力解决这个问题。

在Java中:java.util.concurrent包包含一些不好的已知类,这可能有助于编写确定性的JUnittesting。

看一下

  • CountDownLatch
  • 信号

我有testing线程代码的不幸的任务,他们肯定是我写过的最难的testing。

在编写我的testing时,我使用了代表和事件的组合。 基本上,这是关于使用PropertyNotifyChanged事件与WaitCallback或某种ConditionalWaiter轮询。

我不确定这是否是最好的方法,但它已经为我解决了。

在“ 清理代码”第13章中有一本书,其中有一整部分专门介绍testingmultithreading代码以及一般性的并发性,这可能会帮助您devise更好的multithreading代码。

对于J2E代码,我使用SilkPerformer,LoadRunner和JMeter进行线程的并发testing。 他们都做同样的事情。 基本上,他们给你一个相对简单的接口来pipe理他们的代理服务器的版本,为了分析TCP / IP数据stream,模拟多个用户同时向你的应用服务器发出请求。 代理服务器可以让你处理请求后,通过呈现发送给服务器的整个页面和URL以及来自服务器的响应,来分析所做的请求。

您可以在不安全的http模式下发现一些错误,您可以至less分析正在发送的表单数据,并系统地更改每个用户的表单数据。 但真正的testing是当你运行在https(安全套接字层)。 然后,你还必须有系统地改变会话和cookie数据,这可能会更复杂一些。

在testing并发性时,我发现的最好的错误是,当我发现开发人员依靠Java垃圾回收来login时,build立在login时build立的连接请求到LDAP服务器。这导致用户被暴露到其他用户的会话和非常混乱的结果,当试图分析发生什么事情时,服务器被卷入到膝盖,几乎没有能力完成一个交易,每隔几秒钟。

最后,你或者某个人可能不得不压低和分析我刚才提到的错误代码。 而且,跨部门的公开讨论,就像发生上述问题时发生的那样,是非常有用的。 但是这些工具是testingmultithreading代码的最佳解决scheme。 JMeter是开源的。 SilkPerformer和LoadRunner是专有的。 如果你真的想知道你的应用程序是否线程安全,那么大个子们就是这么做的。 我已经为专业的大公司做了这个,所以我没有猜测。 我是从个人经验来讲的。

谨慎的一句话:理解这些工具确实需要一些时间。 这不是简单地安装软件并启动GUI的问题,除非您已经接触到multithreading编程。 我试图找出要理解的3个关键领域(表格,会话和cookie数据),希望至less从理解这些主题开始,可以帮助您关注快速结果,而不是必须通读整个文档。

您可以使用EasyMock.makeThreadSafe使testing实例线程安全

(如果可能的话)不要使用线程,使用actors / active对象。 易于testing。