在写任何锁之前,我应该对多线程问题进行单元测试吗?

时间:2009-04-26 21:58:05

标签: c# .net multithreading unit-testing tdd

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

这个问题是特定于.NET的

有人要求代码:

public class StackQueue
{
    private Stack<WebRequestInfo> stack = new Stack<WebRequestInfo>();
    private Queue<WebRequestInfo> queue = new Queue<WebRequestInfo>();

    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;
    }
}

10 个答案:

答案 0 :(得分:6)

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

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

答案 1 :(得分:4)

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


好的,现在您已经发布了代码:

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

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

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

答案 2 :(得分:3)

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

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

假设StackQueue实现是线程安全的(我不知道.NET的库),您只需要使Next()线程安全。 Count已经是线程安全的,因为没有客户端可以安全地使用从它返回的值而不使用基于客户端的锁定 - state dependencies between methods否则会破坏代码。

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;
}

答案 3 :(得分:2)

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

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

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

答案 4 :(得分:2)

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

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

答案 5 :(得分:1)

为了清楚起见,并非所有类都需要锁才能保证线程安全。

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

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

答案 6 :(得分:1)

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

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

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

答案 7 :(得分:1)

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

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

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

答案 8 :(得分:0)

大多数单元测试按顺序运行而不是并发运行,因此它们不会暴露并发问题。

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

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

答案 9 :(得分:0)

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

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