使用TDD来编写线程安全的代码

13

如何运用TDD来开发线程安全的代码?例如,我有一个工厂方法,它使用延迟初始化创建一个类的实例,并在此之后返回它:

private TextLineEncoder textLineEncoder;
...
public ProtocolEncoder getEncoder() throws Exception {
    if(textLineEncoder == null)
        textLineEncoder = new TextLineEncoder();
    return textLineEncoder;
}

现在,我想以良好的TDD风格编写一个测试,强制使这段代码具有线程安全性。具体来说,当两个线程同时调用此方法时,我不希望创建两个实例并且仅丢弃其中的一个。虽然很容易实现,但如何编写一个测试来引导我完成它呢?

我是在Java中提问,但答案应该更广泛适用。


谢谢。今天早上写一个这样的工厂时遇到了这个问题。在讨论双重检查锁定机制时曾经出现过,但那是 SO 出现之前的事情了。 - Don Branson
6个回答

5
你可以注入一个“提供者”(一个非常简单的工厂),它只负责这一行代码:
 textLineEncoder = new TextLineEncoder();

那么你的测试将会注入一个非常慢的提供者实现。这样,测试中的两个线程更容易发生冲突。你甚至可以让第一个线程等待一个由第二个线程释放的信号量。然后测试的成功将确保等待的线程超时。通过给第一个线程一个领先优势,你可以确保它在第二个线程释放之前就开始等待。


1
+1 我喜欢这个。一个非常慢的TextLineEncoder构造函数几乎可以保证会引出这里的并发问题。ITextLineEncoderProvider实现可以很容易地模拟这种情况。 - Wim Coenen
而且,作为额外的好处,您可以获得灵活性和可扩展性(可以使用不同的文本行编码器),以及更加专注的测试(可以使用TextLineEncoder模拟)。 - Wim Coenen
1
好主意...而且它甚至可以在不使用缓慢的TextLineEncoder工厂的情况下实现,通过使用支持模拟构造函数的模拟工具:在第一次调用构造函数(第一个线程)时,它将简单地阻塞;然后如果第二个线程进来并执行构造函数第二次,测试将失败。 - Rogério

3

多么有趣的想法!也许这是一个方面,如果第二次调用TextLineEncoder()就会失败。嗯... - Don Branson

2

最近,当我测试一个需要线程安全的实现时,我提供了这个问题的答案。希望这能帮到你,即使那里没有测试。希望链接没问题,而不是重复答案...


是的,链接就可以了。我真的很喜欢你的答案——这是我听到的第一个感觉正确的答案。也许是因为其他答案中有许多依赖于非确定性行为。特别有说服力的是你的评论:“首先,你现有的类只有一个职责,那就是提供一些功能。”一个对象知道它的线程安全性就像它知道它在链表或Web容器中一样。它知道它所在的位置,对于对象来说,这是不好的。我将尝试你的方法,看看效果如何。 - Don Branson

2
在书籍《Clean Code》中,有一些关于如何测试并发代码的技巧。其中一个帮助我发现并发错误的技巧是同时运行比CPU核心数更多的测试用例。
我的项目中,在我的四核机器上运行测试需要大约2秒钟的时间。当我想要测试并发部分(有一些测试用例需要这样做)时,我会按住IntelliJ IDEA中运行所有测试用例的热键,直到状态栏中显示有20、50或100个测试用例正在执行。我会在Windows任务管理器中跟踪CPU和内存使用情况,以找出所有测试用例都已完成执行的时间点(当它们全部运行时,内存使用量会增加1-2 GB,然后缓慢回落)。
然后我逐一关闭所有测试运行输出对话框,并检查是否有失败的测试或死锁的测试,然后我会对它们进行调查,直到找到错误并加以修复。这帮助我发现了几个恶意并发性错误。当面临不应该发生的异常/死锁时,最重要的是始终假设代码已经损坏,并无情地调查原因并加以修复。没有宇宙射线会导致程序随机崩溃——代码中的错误会导致程序崩溃。
还有一些框架,例如http://www.alphaworks.ibm.com/tech/contest,使用字节码操作来强制代码进行更多的线程切换,从而增加使并发错误变得明显的可能性。

1

Java并发编程实践的第12章名为“测试并发程序”。它记录了安全性和活性的测试,但表示这是一个难题。我不确定该章节中的工具是否能够解决这个问题。


0

仅凭直觉,您可以比较返回的实例,以查看它们是否确实是同一实例还是不同实例?这可能是我用C#开始的地方,我想您可以在Java中做同样的事情


听起来是个有用的开始。编写这种测试很容易,但是要编写一个由于缺乏线程同步而失败的测试就比较困难了。 - Morendil
1
我认为这是一个不错的开始 - 但如果在线程构造和返回之间发生切换,它们可能会创建两个实例,并仍然同时返回相同的实例。 - Don Branson
这是正确的。您需要确保在测试系统周围有适当的锁定。 - Sean Chambers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接