46

我正在考虑改进一个包,当它的输入在多个工作线程之间共享时,我认为它不是线程安全的。根据 TDD 原则,我应该编写一些一开始就失败的测试,这些测试肯定对评估问题很有用。

我意识到这不是一件容易实现的事情,而且天真地,多线程测试将是不确定的,因为操作系统将确定调度和各种操作交错的确切顺序。我过去看过并使用过MultithreadedTC,这很有用。然而,在那种情况下,我提前知道现有实现在哪里失败,因此能够编写一组很好的测试来覆盖它。

但是,如果您还没有确切地知道问题出在哪里,是否有一种很好的方法可以编写一个很有可能引发任何潜在问题的测试?是否有其他人发现有帮助的库?我认为从纯粹主义者的角度来看,多线程测试用例应该与通常的单线程测试相同的调用和断言是正确的吗,只在适当的情况下使用多个工作线程运行?

任何关于工具/最佳实践/哲学的提议都将受到欢迎。

4

5 回答 5

24

Java Concurrency in Practice提供了一些关于如何为并发问题编写测试的重要信息。但是,它们不是真正的单元测试。几乎不可能为并发问题编写真正的单元测试。

基本上可以归结为这一点。创建一堆测试线程并启动它们。每个线程应该

  • 等待倒计时锁存器
  • 重复调用一些修改相关可变状态的方法
  • 倒计时第二个锁存器并退出

junit 线程创建所有线程并启动它们,然后在第一个锁存器上倒计时一次以让它们全部离开,然后等待第二个锁存器,然后对可变状态进行一些断言。

甚至比其他类型的错误更重要的是,在找到错误之后为并发错误编写失败的单元测试更容易。

于 2009-01-08T17:45:14.017 回答
16

忘记通过测试并发问题来获得好的结果。尝试减少同步并使问题更小。然后使用尽可能高的库支持来做同步。只有当你真的有然后尝试自己处理并发。当您知道每个工作人员都正确地完成了工作并且您的所有想法都告诉您已经解决了并发问题时,就会产生一些有趣的负载。单元测试框架及其扩展可以完成这项工作,但要知道您不再测试任何单元。(请记住,您已经涵盖了该部分)

如果您的并发模型变得复杂,请查看适合SPIN之类的工具。

于 2009-01-08T15:52:38.347 回答
15

您必须解决两个基本问题。首先是:如何产生线程冲突?其次,你如何验证你的测试?

第一个很简单。使用大锤子。编写一些能够检测一个线程踩到另一个线程的代码,并在 50 个连续线程上运行该代码 50 次左右。您可以使用倒计时锁存器执行此操作:

public void testForThreadClash() {
  final CountDownLatch latch = new CountDownLatch(1);
  for (int i=0; i<50; ++i) {
    Runnable runner = new Runnable() {
      public void run() {
        try {
          latch.await();
          testMethod();
        } catch (InterruptedException ie) { }
      }
    }
    new Thread(runner, "TestThread"+i).start();
  }
  // all threads are waiting on the latch.
  latch.countDown(); // release the latch
  // all threads are now running concurrently.
}

您的 testMethod() 必须生成一种情况,在这种情况下,如果没有同步块,某个线程将踩到另一个线程的数据。它应该能够检测到这种情况并抛出异常。(上面的代码没有这样做。异常很可能会在其中一个测试线程上生成,您需要放入一种机制来在主线程上检测它,但这是一个单独的问题。我留下了它为简单起见,但您可以使用第二个锁存器来执行此操作,测试可以在异常时过早清除它。)

第二个问题比较棘手,但有一个简单的解决方案。问题是:你怎么知道你的锤子会产生碰撞?当然,你的代码有同步块来防止冲突,所以你不能回答这个问题。但是你可以。只需删除同步的关键字。由于使您的类线程安全的是 synchronized 关键字,因此您可以删除它们并重新运行测试。如果测试有效,它现在将失败。

当我第一次编写上面的代码时,结果证明它是无效的。我从没见过冲突。所以它(还)不是一个有效的测试。但是现在我们知道如何得到第二个问题的明确答案了。我们得到了错误的答案,但我们现在可以修改测试以生成我们正在寻找的失败。这就是我所做的:我只是连续运行了 100 次测试。

for (int j=0; j<100; ++j) {
  testForThreadClash();
}

现在我的测试在大约第 20 次迭代时可靠地失败了。这证实了我的测试是有效的。我现在可以恢复 synchronized 关键字并重新运行测试,确信它会告诉我我的类是否是线程安全的。

于 2013-02-08T21:14:21.877 回答
1

在某些情况下,我发现我可以通过使用彼此同步的多个线程(可能使用 CountDownLatch 或某种此类并发机制)来强制对潜在的非线程安全类的调用进行特定的有问题的交错。然而,有时这只是行不通,例如,如果您试图测试如果两个线程同时在同一个方法中会发生什么。

这是一篇有趣的文章(虽然对该工具了解不多):http ://today.java.net/pub/a/today/2003/08/06/multithreadedTests.html

于 2009-01-08T15:59:57.910 回答
0

如果线程是您正在测试的代码的重点(想想并发数据结构),那么对多线程代码进行单元测试就没有错。一般的方法是阻塞主测试线程,在单独的线程中执行断言,在主线程中捕获并重新抛出任何失败的断言,否则解除阻塞主测试线程以便测试可以完成。使用ConcurrentUnit非常简单:

@Test
public void shouldDeliverMessage() throws Throwable {
  final Waiter waiter = new Waiter();

  messageBus.registerHandler(message -> {
    // Called on separate thread
    waiter.assertEquals(message, "foo");

    // Unblocks the waiter.await call
    waiter.resume();
  };

  messageBus.send("foo");

  // Wait for resume() to be called
  waiter.await(1000);
}

这里的关键是,任何线程中的任何失败断言都将由 重新抛出到主线程中waiter,从而允许测试通过或失败。

于 2016-04-20T18:20:49.570 回答