线程安全性的单元测试?

68

我已经编写了一个类和许多单元测试,但是我没有使其线程安全。现在,我想要使这个类线程安全,但为了证明它并使用TDD,我想在开始重构之前编写一些失败的单元测试。

有什么好办法吗?

我的第一个想法是创建几个线程,并使它们以不安全的方式使用该类。用足够多的线程进行足够多的次数操作,我就有可能看到它崩溃。


我以前按照你描述的方式进行过单元测试,但总觉得结果中存在一定的随机性,所以我会很感兴趣地关注这里的答案 :o) - Fredrik Mörk
3
好的,我会尽力进行翻译。以下是需要翻译的内容:Duplicate: https://dev59.com/MnI-5IYBdhLWcg3wta1q重复:https://dev59.com/MnI-5IYBdhLWcg3wta1q - JeffH
12
是的,你说得对。Pffffff,别试图成为 Stack Overflow 的警察。 - Josh Stodola
9个回答

21

有两个产品可以帮助你:

  • Microsoft Chess:可通过单元测试检查代码死锁,同时我认为它也能检查竞争条件。
  • Typemock Racer:可通过单元测试检查代码死锁。

使用这两种工具很简单-编写一个简单的单元测试,运行你的代码多次并检查你的代码是否存在死锁/竞争条件。

编辑: 谷歌发布了一款名为ThreadSanitizer的工具,它可以在运行时(而非测试期间)检查竞争条件。
它可能无法找到所有竞争条件,因为它只分析当前运行而非所有可能的情况,但是一旦发生竞争条件,该工具可能会帮助你找到它。

更新: Typemock网站不再提供Racer的下载链接,而且该工具已经4年未更新。我猜这个项目已经关闭了。


我认为 MS Chess 链接已经失效了。请尝试使用这个链接:http://research.microsoft.com/en-us/projects/chess/default.aspx - jpbochi
1
Typemock Racer 似乎也出现了故障。请尝试访问此链接:http://site.typemock.com/typemock-racer - jpbochi
MS Chess的链接目前似乎可以使用。修复了Typemock Racer的链接。 - Samir Talwar

10

问题在于大多数多线程问题(如竞争条件)本质上是不确定的。它们可能依赖于硬件行为,而这是您无法模拟或触发的。

这意味着,即使您使用多个线程进行测试,如果您的代码存在缺陷,它们也不会一致地失败。


“...硬件行为...不可能模拟...”的情况下,似乎至少对于较简单的情况,交错访问组合的数量是有限的,并且可以通过某种方式枚举,然后通过一些带有仪器的线程调度程序强制执行每个组合。有些声明称Chess覆盖率达到100%。如果这是真的,那么硬件就不应该成为一个因素。 - crokusek

5
请注意,Dror的回答没有明确说明这一点,但至少Chess(可能还有Racer)是通过运行一组线程来获得可重复错误的所有可能交错。它们不仅仅是希望如果出现错误就会发生巧合而运行线程。

例如,Chess将运行所有的交错,然后给您一个标记字符串,表示在哪个交错上找到了死锁,以便您可以将测试与从死锁角度来看有趣的特定交错相关联。

我不知道这个工具的确切内部工作原理,以及它如何将这些标记字符串映射回您可能正在更改以修复死锁的代码,但是这就是情况... 我真的很期待这个工具(和Pex)成为VS IDE的一部分。


3
我曾看到有人尝试使用标准的单元测试来测试并发问题,就像你自己提出的那样。然而,这种测试速度很慢,并且到目前为止未能识别出我们公司所遇到的任何一种并发问题。
在经历了许多失败之后,尽管我非常喜欢单元测试,但我已经接受了错误的事实:并发问题不是单元测试的优势之一。我通常鼓励对涉及并发的类进行分析和审查,而不是使用单元测试。如果完全了解系统,往往可以证明/反驳线程安全性的主张。
总之,我希望有人能给我一些可能指向相反方向的东西,所以我密切关注这个问题。

2

最近我也遇到了同样的问题,我的解决方法是:首先,你现有的类只负责提供一些功能,它不应该担心线程安全。如果需要线程安全,应该使用其他对象来提供此功能。但是,如果其他对象提供线程安全性,则不能将其视为可选项,否则无法证明代码是线程安全的。因此,这是我的处理方式:

// This interface is optional, but is probably a good idea.
public interface ImportantFacade
{
    void ImportantMethodThatMustBeThreadSafe();
}

// This class provides the thread safe-ness (see usage below).
public class ImportantTransaction : IDisposable
{
    public ImportantFacade Facade { get; private set; }
    private readonly Lock _lock;

    public ImportantTransaction(ImportantFacade facade, Lock aLock)
    {
        Facade = facade;
        _lock = aLock;
        _lock.Lock();
    }

    public void Dispose()
    {
        _lock.Unlock();
    }
}

// I create a lock interface to be able to fake locks in my tests.
public interface Lock
{
    void Lock();
    void Unlock();
}

// This is the implementation I want in my production code for Lock.
public class LockWithMutex : Lock
{
    private Mutex _mutex;

    public LockWithMutex()
    {
        _mutex = new Mutex(false);
    }

    public void Lock()
    {
        _mutex.WaitOne();
    }

    public void Unlock()
    {
        _mutex.ReleaseMutex();
    }
}

// This is the transaction provider. This one should replace all your
// instances of ImportantImplementation in your code today.
public class ImportantProvider<T> where T:Lock,new()
{
    private ImportantFacade _facade;
    private Lock _lock;

    public ImportantProvider(ImportantFacade facade)
    {
        _facade = facade;
        _lock = new T();
    }

    public ImportantTransaction CreateTransaction()
    {
        return new ImportantTransaction(_facade, _lock);
    }
}

// This is your old class.
internal class ImportantImplementation : ImportantFacade
{
    public void ImportantMethodThatMustBeThreadSafe()
    {
        // Do things
    }
}

使用泛型可以在测试中使用一个假锁来验证在创建事务时始终获取锁并且在事务被处理前不会释放。现在您还可以验证在调用重要方法时是否已获取锁。在生产代码中的使用应该如下所示:

// Make sure this is the only way to create ImportantImplementation.
// Consider making ImportantImplementation an internal class of the provider.
ImportantProvider<LockWithMutex> provider = 
    new ImportantProvider<LockWithMutex>(new ImportantImplementation());

// Create a transaction that will be disposed when no longer used.
using (ImportantTransaction transaction = provider.CreateTransaction())
{
    // Access your object thread safe.
    transaction.Facade.ImportantMethodThatMustBeThreadSafe();
}

通过确保ImportantImplementation无法被他人创建(例如在提供程序中创建并将其设置为私有类),您现在可以证明您的类是线程安全的,因为它无法在没有事务的情况下访问,而事务在创建时始终获取锁,并在释放时释放锁。

确保正确处理事务的释放可能更加困难,如果未正确释放,您可能会在应用程序中看到奇怪的行为。您可以使用Microsoft Chess等工具(如另一个答案中建议的)来查找此类问题。或者您可以让提供程序实现facade并使其像这样实现:

    public void ImportantMethodThatMustBeThreadSafe()
    {
        using (ImportantTransaction transaction = CreateTransaction())
        {
            transaction.Facade.ImportantMethodThatMustBeThreadSafe();
        }
    }

虽然这是我希望你能实现的代码,但你需要自己设计测试用例以验证这些类是否符合要求。


1

虽然这种方法不像使用Racer或者Chess这样的工具那么优雅,但我曾经使用过它来测试线程安全性:

// from linqpad

void Main()
{
    var duration = TimeSpan.FromSeconds(5);
    var td = new ThreadDangerous(); 

    // no problems using single thread (run this for as long as you want)
    foreach (var x in Until(duration))
        td.DoSomething();

    // thread dangerous - it won't take long at all for this to blow up
    try
    {           
        Parallel.ForEach(WhileTrue(), x => 
            td.DoSomething());

        throw new Exception("A ThreadDangerException should have been thrown");
    }
    catch(AggregateException aex)
    {
        // make sure that the exception thrown was related
        // to thread danger
        foreach (var ex in aex.Flatten().InnerExceptions)
        {
            if (!(ex is ThreadDangerException))
                throw;
        }
    }

    // no problems using multiple threads (run this for as long as you want)
    var ts = new ThreadSafe();
    Parallel.ForEach(Until(duration), x => 
        ts.DoSomething());      

}

class ThreadDangerous
{
    private Guid test;
    private readonly Guid ctrl;

    public void DoSomething()
    {           
        test = Guid.NewGuid();
        test = ctrl;        

        if (test != ctrl)
            throw new ThreadDangerException();
    }
}

class ThreadSafe
{
    private Guid test;
    private readonly Guid ctrl;
    private readonly object _lock = new Object();

    public void DoSomething()
    {   
        lock(_lock)
        {
            test = Guid.NewGuid();
            test = ctrl;        

            if (test != ctrl)
                throw new ThreadDangerException();
        }
    }
}

class ThreadDangerException : Exception 
{
    public ThreadDangerException() : base("Not thread safe") { }
}

IEnumerable<ulong> Until(TimeSpan duration)
{
    var until = DateTime.Now.Add(duration);
    ulong i = 0;
    while (DateTime.Now < until)
    {
        yield return i++;
    }
}

IEnumerable<ulong> WhileTrue()
{
    ulong i = 0;
    while (true)
    {
        yield return i++;
    }
}

理论上,如果您可以在非常短的时间内一致地引发线程危险条件,那么您应该能够通过等待相对较长的时间而不观察状态损坏来实现线程安全条件并进行验证。

我承认这可能是一个原始的方法,并且在复杂情况下可能没有帮助。


1

1

你需要为每个并发场景构建一个测试用例;这可能需要用较慢的等效操作(或模拟)替换高效操作,并在循环中运行多个测试,以增加争用的机会。

没有具体的测试用例,很难提出具体的测试方案。

一些可能有用的参考资料:


0
这是我的方法。这个测试不关心死锁,而是关心一致性。我正在测试一个带有同步块的方法,代码看起来像这样:
synchronized(this) {
  int size = myList.size();
  // do something that needs "size" to be correct,
  // but which will change the size at the end.
  ...
}

很难制造一个可靠地产生线程冲突的情况,但这是我所做的。

首先,我的单元测试创建了50个线程,同时启动它们,并让它们都调用我的方法。我使用CountDown Latch来同时启动它们:

CountDownLatch latch = new CountDownLatch(1);
for (int i=0; i<50; ++i) {
  Runnable runner = new Runnable() {
    latch.await(); // actually, surround this with try/catch InterruptedException
    testMethod();
  }
  new Thread(runner, "Test Thread " +ii).start(); // I always name my threads.
}
// all threads are now waiting on the latch.
latch.countDown(); // release the latch
// all threads are now running the test method at the same time.

这可能会产生冲突,也可能不会。如果发生冲突,我的testMethod()应该能够抛出异常。但我们还不能确定这是否会产生冲突。因此,我们不知道测试是否有效。所以这是个技巧:注释掉同步关键字并运行测试。如果这导致冲突,则测试将失败。如果没有同步关键字而失败,则您的测试有效。

我就是这样做的,我的测试没有失败,因此它(尚)不是一个有效的测试。但是,我可以通过将上面的代码放在循环内部,并连续运行100次来可靠地产生故障。所以我调用了该方法5000次。(是的,这将产生一个缓慢的测试。不要担心。您的客户不会受到影响,所以您也不应该。)

一旦我将这段代码放入外部循环中,我就能够在外部循环的第20次迭代时可靠地看到故障。现在我有信心测试是有效的,并恢复了同步关键字以运行实际测试。(它成功了。)

您可能会发现测试在一台机器上是有效的,在另一台机器上却无效。如果测试在一台机器上有效,并且您的方法通过了测试,则可以认为它在所有机器上都是线程安全的。但是您应该在运行夜间单元测试的机器上进行有效性测试。

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