到目前为止,我已经避免了测试多线程代码的噩梦,因为它似乎是一个太大的雷区。我想知道人们是如何测试依赖于线程的代码以获得成功执行的,或者人们是如何测试那些仅在两个线程以给定方式交互时才会出现的问题的?

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


当前回答

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

我从编写FindBugs的同一组中找到了多线程TC Java库。它允许您在不使用Sleep()的情况下指定事件的顺序,而且它是可靠的。我还没试过。

这种方法的最大限制是它只允许您测试您怀疑会引起麻烦的场景。正如其他人所说,您确实需要将多线程代码隔离到少量简单类中,以便有希望彻底测试它们。

一旦您仔细测试了您预计会导致问题的场景,那么在类中抛出一堆并发请求的不科学测试是寻找意外问题的好方法。

更新:我已经玩了一些多线程TC Java库,它工作得很好。我还将它的一些特性移植到一个。net版本,我称之为TickingTest。

其他回答

上周我花了大部分时间在大学图书馆学习并发代码的调试。核心问题是并发代码是不确定的。通常,学术调试可以分为三个阵营之一:

Event-trace/replay. This requires an event monitor and then reviewing the events that were sent. In a UT framework, this would involve manually sending the events as part of a test, and then doing post-mortem reviews. Scriptable. This is where you interact with the running code with a set of triggers. "On x > foo, baz()". This could be interpreted into a UT framework where you have a run-time system triggering a given test on a certain condition. Interactive. This obviously won't work in an automatic testing situation. ;)

现在,正如上面评论者所注意到的,您可以将并发系统设计成更确定的状态。然而,如果你做得不好,你就又回到了设计顺序系统的问题上。

我的建议是,专注于制定一个非常严格的设计协议,规定什么是线程,什么不是线程。如果你限制了你的接口,使元素之间的依赖最小化,那就容易多了。

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

等待在帮助您编写确定性单元测试时也很有用。它允许您等待系统中的某个状态更新。例如:

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

or

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

它还支持Scala和Groovy。

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

并发是内存模型、硬件、缓存和代码之间复杂的相互作用。在Java的情况下,至少这样的测试主要由jcstress部分解决。众所周知,该库的创建者是许多JVM、GC和Java并发特性的作者。

但是即使是这个库也需要对Java内存模型规范有很好的了解,这样我们才能确切地知道我们在测试什么。但我认为这项工作的重点是微基准测试。不是庞大的业务应用。

运行多个线程并不困难;这是小菜一碟。不幸的是,线程通常需要彼此通信;这就是困难所在。

最初发明的允许模块之间通信的机制是函数调用;当模块A想要与模块B通信时,它只调用模块B中的一个函数。不幸的是,这对线程不起作用,因为当你调用一个函数时,该函数仍然运行在当前线程中。

为了克服这个问题,人们决定采用一种更原始的通信机制:只声明一个特定的变量,并让两个线程都可以访问该变量。换句话说,允许线程共享数据。分享数据是人们自然而然想到的第一件事,这似乎是一个不错的选择,因为它看起来非常简单。我是说,能有多难,对吧?会出什么问题呢?

竞态条件。这就是可能、也将会出错的地方。

当人们意识到他们的软件由于竞争条件而遭受随机的、不可复制的灾难性失败时,他们开始发明复杂的机制,如锁和比较-交换,旨在防止此类事情的发生。这些机制属于广义的“同步”范畴。不幸的是,同步有两个问题:

这是很难做到的,所以很容易出现bug。 它是完全不可测试的,因为您无法测试竞态条件。

精明的读者可能会注意到“非常容易出现bug”和“完全不可测试”是一个致命的组合。

现在,在自动化软件测试的概念变得流行之前,我上面提到的机制已经被行业的大部分人发明和采用了;所以,没有人知道这个问题有多致命;他们只是认为这是一个很难的主题,需要高手程序员,每个人都能接受。

如今,无论我们做什么,我们都把测试放在第一位。所以,如果某些机制是不可测试的,那么使用该机制就是不可能的。因此,同步已经失宠;现在还在练的人已经很少了,而且练的人一天比一天少。

没有同步线程就不能共享数据;然而,最初的要求不是共享数据;它允许线程之间进行通信。除了共享数据之外,还存在其他更优雅的线程间通信机制。

其中一种机制是消息传递,也称为事件。

对于消息传递,整个软件系统中只有一个地方利用了同步,那就是我们用来存储消息的并发阻塞队列收集类。(我们的想法是,我们应该至少能把那一小部分做对。)

消息传递的优点是它不受竞态条件的影响,并且是完全可测试的。

看看我的相关答案在

为自定义Barrier设计一个Test类

它偏向于Java,但对选项进行了合理的总结。

总而言之(我认为),它不是使用一些花哨的框架来确保正确性,而是如何设计你的多线程代码。拆分关注点(并发性和功能性)有助于提高信心。测试引导的面向对象软件的发展比我能更好地解释一些选项。

静态分析和形式化方法(参见并发性:状态模型和Java程序)是一种选择,但我发现它们在商业开发中用处有限。

不要忘记,任何加载/浸泡风格的测试都很少能保证突出问题。

好运!