单元测试的线程安全性?


68

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

有什么好办法吗?

我的第一个想法就是创建几个线程,并使它们全部以不安全的方式使用该类。用足够的线程执行此操作足够的时间,我一定会看到它破裂。


我已经按照您之前描述的方式进行了单元测试,但是始终觉得结果中存在随机性,因此我将感兴趣地关注这里的答案:o)
FredrikMörk,2009年


12
@JeffH是的,你说对了。Pffffff,退出尝试成为一名特警。
Josh Stodola,2009年

Answers:


21

有两种产品可以为您提供帮助:

两者都检查代码中的死锁(通过单元测试),我认为国际象棋也检查竞争条件。

使用这两种工具都很容易-您编写一个简单的单元测试,然后多次运行代码,并检查代码中是否可能出现死锁/争用条件。

编辑: Google发布了一个用于在运行时(而不是在测试过程中)检查竞争状况的工具,称为thread-race-test
它不会找到所有竞争条件,因为它仅分析当前运行情况,而不是分析所有可能的情况,例如上面的工具,但是一旦发生情况,它可能会帮助您找到竞争条件。

更新: Typemock网站不再具有Racer的链接,并且在过去4年中未更新。我想这个项目已经关闭。


我认为MS Chess链接已损坏。尝试以下一种方法:research.microsoft.com/en-us/projects/chess/default.aspx
jpbochi 2010年

1
Typemock赛车手似乎也坏了。试试这个:site.typemock.com/typemock-racer
jpbochi 2010年

到目前为止,MS Chess链接似乎仍然有效。修复了Typemock Racer链接。
萨米尔·塔尔瓦尔

10

问题在于,大多数多线程问题(例如竞争条件)就其性质而言都是不确定的。它们可能取决于您无法模拟或触发的硬件行为。

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


-1表示“ ...硬件行为...可能无法仿真...”。似乎至少在较简单的情况下,交织访问组合的数量是有限的,并且可以被枚举(尽管是“某种方式”),然后迫使代码通过某些检测线程调度程序来执行每种组合。一些声明说国际象棋覆盖率是100%。如果是这样,那么硬件就不应该成为一个因素。
crokusek

5

请注意,Dror的答案并未明确说明这一点,但是至少Chess(可能是Racer)通过在所有可能的交织中运行一组线程来获得可重现的错误,从而工作。他们不只是运行线程一会儿,希望如果发生错误,它会偶然发生。

例如,国际象棋将遍历所有交错,然后为您提供一个标记字符串,该字符串表示在其上发现死锁的交错,以便您可以从死锁的角度将测试归因于特定的交错。

我不知道此工具的确切内部工作原理,也不知道它如何将这些标记字符串映射回您可能要更改以修复死锁的代码,但是您已经掌握了...我实际上很期待这个工具(和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等工具(如另一分析工具中所述)来查找类似的内容。或者,您可以让提供者实现外观,并使其实现如下:

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

从理论上讲,如果您可以在很短的时间内导致线程危险状况持续发生,那么您应该能够带来线程安全状况并通过等待相对较长的时间来验证它们,而不会观察到状态损坏。

我确实承认这可能是解决问题的原始方法,并且在复杂的情况下可能无济于事。


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()应​​该能够引发异常。但是我们尚不能确定这会产生冲突。因此我们不知道测试是否有效。因此,这就是窍门:注释掉您的同步关键字并运行测试。如果这产生冲突,则测试将失败。如果在没有synced关键字的情况下失败,则您的测试有效。

那就是我所做的,并且我的测试没有失败,因此(还)不是有效的测试。但是通过将代码放在循环中并连续运行100次,我能够可靠地产生故障。因此,我将该方法调用了5000次。(是的,这将产生一个缓慢的测试。不必担心。您的客户不会对此感到烦恼,因此您也不应这样做。)

一旦将此代码放入外部循环中,便能够可靠地看到外部循环第20次迭代失败。现在,我确信测试是有效的,并且我恢复了同步关键字以运行实际测试。(有效。)

您可能会发现测试在一台计算机上有效,而在另一台计算机上无效。如果测试在一台机器上有效,并且您的方法通过了测试,那么大概在所有机器上都是线程安全的。但是,您应该在运行夜间单元测试的计算机上测试有效性。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.