在写任何锁之前,我应该对multithreading问题进行unit testing吗?

我正在编写一个我知道需要锁的类,因为该类必须是线程安全的。 但是,因为我是测试驱动 – 开发我知道在为它创建测试之前我不能编写一行代码。 而且我发现很难做到,因为测试最终会变得非常复杂。 在这些情况下你通常做什么? 有什么工具可以帮助吗?

这个问题是.NET特定的

有人要求代码:

public class StackQueue { private Stack stack = new Stack(); private Queue queue = new Queue(); public int Count { get { return this.queue.Count + this.stack.Count; } } public void Enqueue(WebRequestInfo requestInfo) { this.queue.Enqueue(requestInfo); } public void Push(WebRequestInfo requestInfo) { this.stack.Push(requestInfo); } private WebRequestInfo Next() { if (stack.Count > 0) { return stack.Pop(); } else if (queue.Count > 0) { return queue.Dequeue(); } return null; } } 

在编写multithreading代码时,您必须比平常更多地使用您的大脑。 您必须逻辑地对每一行代码进行推理,无论它是否是线程安全的。 这就像certificate数学公式的正确性 – 你不能通过给出公式为真的N值的例子来certificate像“N + 1> N对所有N”这样的事情。 类似地,通过编写试图暴露问题的测试用例,不可能certificate类是线程安全的。 通过测试,它只能certificate存在故障,而不是没有故障。

您可以做的最好的事情是最小化对multithreading代码的需求。 优选地,应用程序应该没有multithreading代码(例如,依赖于线程安全库和合适的设计模式),或者应该限制在非常小的区域。 您的StackQueue类看起来很简单,因此您可以通过一点思考使其安全地进行线程安全。

假设StackQueue实现是线程安全的(我不知道.NET的库),你只需要使Next()线程安全。 Count已经是线程安全的,因为没有客户端可以安全地使用从它返回的值而不使用基于客户端的锁定 – 否则方法之间的状态依赖性会破坏代码。

Next()不是线程安全的,因为它在方法之间具有状态依赖性。 如果线程T1和T2同时调用stack.Count并返回1,则其中一个将使用stack.Pop()获取值,但另一个将在堆栈为空时调用stack.Pop()然后似乎抛出InvalidOperationException )。 您将需要一个堆栈和队列,其中包含非阻塞版本的Pop()Dequeue() (在空时返回null)。 然后代码在这样编写时是线程安全的:

 private WebRequestInfo Next() { WebRequestInfo next = stack.PopOrNull() if (next == null) { next = queue.DequeueOrNull(); } return next; } 

好吧,在释放门之前,你通常可以使用像ManualResetEvent这样的东西让一些线程进入预期的问题状态……但是这只涉及一小部分线程问题。

对于线程错误的更大问题,有CHESS (进行中) – 可能是未来的一个选项。

您不应该在unit testing中真正测试线程安全性。 您应该对线程安全性进行单独的压力测试。


好的,既然您已经发布了代码:

 public int Count { get { return this.queue.Count + this.stack.Count; } } 

这是一个很好的例子,说明在编写unit testing时会遇到问题,这会在代码中暴露线程问题。 此代码可能需要同步,因为this.queue.Count和this.stack.Count的值可以在计算总计的中间更改,因此它可以返回不“正确”的值。

但是 – 鉴于类定义的其余部分,实际上没有任何东西依赖于Count给出一致的结果,那么它是否真的很重要,如果它是“错误的”? 如果不知道程序中的其他类如何使用这个类,就无法知道。 这使得测试线程问题成为集成测试,而不是unit testing。

TDD是一种工具 – 而且是一种很好的工具 – 但有时你会遇到使用特定工具无法很好解决的问题。 我建议,如果开发测试过于复杂,你应该使用TDD开发预期的function,但也许可以依靠代码检查来确保你自己,你添加的锁定代码是合适的,并允许你的类是线程-安全。

一个可能的解决方案是开发将进入锁内并将其放入自己的方法中的代码。 然后你可以伪造这个方法来测试你的锁定代码。 在您的假代码中,您可以简单地建立一个等待,以确保访问代码的第二个线程必须等待锁定,直到第一个线程完成。 如果不知道你的代码到底做了什么,我就不能比这更具体了。

multithreading可能导致如此复杂的问题,几乎不可能为它们编写unit testing。 您可能设法编写一个unit testing,当在代码上执行时,该unit testing具有100%的失败率但在您通过之后,很可能在代码中仍存在竞争条件和类似问题。

线程问题的问题在于它们是随机出现的。 即使您通过unit testing,也不一定意味着代码可以正常工作。 所以在这种情况下,TDD会给人一种虚假的安全感,甚至可能被认为是一件坏事。

并且值得记住的是,如果一个类是线程安全的,你可以从几个线程中使用它而没有问题 – 但是如果一个类不是线程安全的,它并不会立即暗示你不能在没有问题的情况下从几个线程中使用它。 它在实践中仍然可以是线程安全的,但没有人只是想承担它不是线程安全的责任。 如果它在实践中是线程安全的,那么编写因multithreading而失败的unit testing是不可能的。 (当然,大多数非线程安全类实际上都不是线程安全的,并且会很快失败。)

需要明确的是,并非所有类都需要锁才能保证线程安全。

如果您的测试最终过于复杂,那么可能是您的课程或方法过于复杂,两者紧密耦合或承担过多责任的症状。 尽量遵循单一责任原则。

您是否介意发布有关您class级的更多具体信息?

特别是对于多核系统,您通常可以测试线程问题,而不是确定性。

我通常这样做的方法是启动多个线程,通过相关代码爆炸并计算意外结果的数量。 这些线程运行一段很短的时间(通常为2-3秒),然后使用Interlocked.CompareExchange添加结果,并正常退出。 我的测试将它们旋转然后在每个上调用.Join,然后检查错误的数量是否为0。

当然,这不是万无一失的,但是对于多核CPU,它通常能够很好地向我的同事certificate存在需要用对象解决的问题(假设预期用途需要multithreading访问) 。

我同意其他海报应该避免使用multithreading代码,或者至少局限于应用程序的一小部分。 但是,我仍然想要一些方法来测试这些小部分。 我正在使用MultithreadedTC Java库的.NET端口。 我的端口称为Ticking Test ,源代码发布在Google Code上。

MultithreadedTC允许您使用标记有TestThread属性的多个方法编写测试类。 每个线程都可以等待某个滴答计数到达,或者断言它认为当前的滴答计数应该是什么。 当所有当前线程被阻塞时,协调器线程会提前滴答计数并唤醒正在等待下一个滴答计数的所有线程。 如果您有兴趣,请查看MultithreadedTC概述以获取示例。 MultithreadedTC是由编写FindBugs的一些人编写的。

我在一个小项目上成功使用了我的端口。 缺少的主要function是我无法在测试期间跟踪新创建的线程。

大多数unit testing按顺序而不是并发操作,因此它们不会暴露并发问题。

具有并发专业知识的程序员进行代码检查是您​​最好的选择。

那些邪恶的并发问题通常不会出现,直到你在该领域有足够的产品来产生一些统计相关的趋势。 有时难以找到它们,因此通常最好避免必须首先编写代码。 如果可能,使用预先存在的线程安全库和完善的设计模式。

  • 您会发现“不变” – 无论客户端线程的数量和交错如何,都必须保持正确。 这是困难的部分。
  • 然后编写一个生成多个线程的测试,练习该方法并声明该不变量仍然存在。 这将失败 – 因为没有代码使其成为线程安全的。 红色
  • 添加代码以使其成为线程安全的并使压力测试通过。 绿色。 根据需要重构。

有关更多详细信息,请参阅GOOS书籍,了解与multithreading代码相关的章节。