到目前为止,我已经避免了测试多线程代码的噩梦,因为它似乎是一个太大的雷区。我想知道人们是如何测试依赖于线程的代码以获得成功执行的,或者人们是如何测试那些仅在两个线程以给定方式交互时才会出现的问题的?
对于今天的程序员来说,这似乎是一个非常关键的问题,恕我直言,将我们的知识集中在这个问题上是很有用的。
到目前为止,我已经避免了测试多线程代码的噩梦,因为它似乎是一个太大的雷区。我想知道人们是如何测试依赖于线程的代码以获得成功执行的,或者人们是如何测试那些仅在两个线程以给定方式交互时才会出现的问题的?
对于今天的程序员来说,这似乎是一个非常关键的问题,恕我直言,将我们的知识集中在这个问题上是很有用的。
当前回答
运行多个线程并不困难;这是小菜一碟。不幸的是,线程通常需要彼此通信;这就是困难所在。
最初发明的允许模块之间通信的机制是函数调用;当模块A想要与模块B通信时,它只调用模块B中的一个函数。不幸的是,这对线程不起作用,因为当你调用一个函数时,该函数仍然运行在当前线程中。
为了克服这个问题,人们决定采用一种更原始的通信机制:只声明一个特定的变量,并让两个线程都可以访问该变量。换句话说,允许线程共享数据。分享数据是人们自然而然想到的第一件事,这似乎是一个不错的选择,因为它看起来非常简单。我是说,能有多难,对吧?会出什么问题呢?
竞态条件。这就是可能、也将会出错的地方。
当人们意识到他们的软件由于竞争条件而遭受随机的、不可复制的灾难性失败时,他们开始发明复杂的机制,如锁和比较-交换,旨在防止此类事情的发生。这些机制属于广义的“同步”范畴。不幸的是,同步有两个问题:
这是很难做到的,所以很容易出现bug。 它是完全不可测试的,因为您无法测试竞态条件。
精明的读者可能会注意到“非常容易出现bug”和“完全不可测试”是一个致命的组合。
现在,在自动化软件测试的概念变得流行之前,我上面提到的机制已经被行业的大部分人发明和采用了;所以,没有人知道这个问题有多致命;他们只是认为这是一个很难的主题,需要高手程序员,每个人都能接受。
如今,无论我们做什么,我们都把测试放在第一位。所以,如果某些机制是不可测试的,那么使用该机制就是不可能的。因此,同步已经失宠;现在还在练的人已经很少了,而且练的人一天比一天少。
没有同步线程就不能共享数据;然而,最初的要求不是共享数据;它允许线程之间进行通信。除了共享数据之外,还存在其他更优雅的线程间通信机制。
其中一种机制是消息传递,也称为事件。
对于消息传递,整个软件系统中只有一个地方利用了同步,那就是我们用来存储消息的并发阻塞队列收集类。(我们的想法是,我们应该至少能把那一小部分做对。)
消息传递的优点是它不受竞态条件的影响,并且是完全可测试的。
其他回答
Testing MT code for correctness is, as already stated, quite a hard problem. In the end it boils down to ensuring that there are no incorrectly synchronised data races in your code. The problem with this is that there are infinitely many possibilities of thread execution (interleavings) over which you do not have much control (be sure to read this article, though). In simple scenarios it might be possible to actually prove correctness by reasoning but this is usually not the case. Especially if you want to avoid/minimize synchronization and not go for the most obvious/easiest synchronization option.
我遵循的一种方法是编写高度并发的测试代码,以使潜在的未检测到的数据竞争有可能发生。然后我运行这些测试一段时间:)我曾经无意中看到一个演讲,一些计算机科学家展示了一个这样做的工具(根据规格随机设计测试,然后疯狂地运行它们,同时检查定义的不变量是否被破坏)。
顺便说一下,我认为测试MT代码的这一方面在这里还没有提到:确定可以随机检查的代码不变量。不幸的是,找到这些不变量也是一个相当困难的问题。此外,它们在执行期间可能不会一直保持,因此您必须找到/强制执行可以期望它们为真的执行点。将代码执行到这样的状态也是一个难题(并且本身可能会引起并发性问题)。呼,这太难了!
一些有趣的链接可供阅读:
确定性交错:允许强制执行某些线程交错,然后检查不变量的框架 jMock Blitzer:压力测试同步 assertConcurrent:压力测试同步的JUnit版本 测试并发代码:简要介绍两种主要方法:蛮力(压力测试)或确定性(使用不变量)
我在测试多线程代码时也遇到了严重的问题。然后我在Gerard Meszaros的“xUnit测试模式”中找到了一个非常酷的解决方案。他描述的模式被称为Humble object。
基本上,它描述了如何将逻辑提取到独立的、易于测试的组件中,该组件与环境解耦。在你测试了这个逻辑之后,你可以测试复杂的行为(多线程,异步执行,等等…)
这个问题发布已经有一段时间了,但仍然没有答案…
Kleolb02的答案很好。我会试着讲得更详细一些。
有一种方法,我在c#代码中练习过。对于单元测试,您应该能够编写可重复的测试,这是多线程代码中的最大挑战。因此,我的回答旨在将异步代码强制到同步工作的测试装置中。
这是Gerard Meszaros的书“xUnit测试模式”中的一个想法,被称为“Humble Object”(第695页):必须将核心逻辑代码和任何闻起来像异步代码的东西分开。这将产生一个用于核心逻辑的类,它同步地工作。
这将使您能够以同步方式测试核心逻辑代码。您可以绝对控制对核心逻辑进行调用的时间,因此可以进行可重复的测试。这就是分离核心逻辑和异步逻辑的好处。
这个核心逻辑需要由另一个类来包装,这个类负责异步接收对核心逻辑的调用,并将这些调用委托给核心逻辑。产品代码将只通过该类访问核心逻辑。因为这个类应该只委托调用,所以它是一个没有太多逻辑的非常“愚蠢”的类。因此,您可以将这个异步工作类的单元测试保持在最小值。
在此之上的任何测试(测试类之间的交互)都是组件测试。同样在这种情况下,如果你坚持使用“Humble Object”模式,你应该能够完全控制时间。
我最近发现了一个叫做Threadsafe的工具(用于Java)。它是一个静态分析工具,很像findbugs,但专门用于发现多线程问题。它不是测试的替代品,但我可以推荐它作为编写可靠的多线程Java的一部分。
它甚至可以捕捉到一些非常微妙的潜在问题,比如类包容、通过并发类访问不安全的对象以及在使用双重检查锁定范式时发现丢失的volatile修饰符。
如果您编写多线程Java,请尝试一下。
我曾经有过测试线程代码的不幸任务,这绝对是我写过的最难的测试。
在编写测试时,我使用委托和事件的组合。基本上,它都是关于使用PropertyNotifyChanged事件和WaitCallback或某种轮询的ConditionalWaiter。
我不确定这是否是最好的方法,但它对我来说是有效的。