线程安全的unit testing?
我已经写了一个类和许多unit testing,但是我没有使它安全。 现在,我想让类的线程安全,但为了certificate它并使用TDD,我想在重新开始之前编写一些失败的unit testing。
任何好办法做到这一点?
我的第一个想法是创build一对夫妇的线程,使他们都以不安全的方式使用这个类。 做足够多的线程足够多的时间,我一定会看到它断裂。
有两种产品可以帮助你:
- 微软国际象棋
- Typemock赛车手
两者都检查代码中的死锁(通过unit testing),我认为国际象棋也会检查竞争情况。
使用这两种工具非常简单 – 您可以编写一个简单的unit testing,并多次运行代码,并检查代码中是否存在死锁/竞态条件。
编辑:谷歌已经发布了一个工具,在运行时(而不是在testing期间)检查调用线程竞赛testing的竞态条件 。
它不会find所有的竞争条件,因为它只分析当前的运行情况,而不是像上面的工具那样的所有可能的情况,但它可能会帮助您find竞争状况。
更新: Typemock网站不再有一个链接到赛车手,并没有在过去的4年更新。 我猜这个项目已经closures了。
问题在于大多数multithreading问题,如竞态条件,本质上都是非确定性的。 他们可以依靠你不可能模拟或触发的硬件行为。
这意味着,即使您使用多个线程进行testing,如果代码中存在缺陷,它们也不会始终如一。
请注意,Dror的答案并没有明确说出这一点,但至less国际象棋(可能是赛车)通过运行一系列线程,通过所有可能的交织来获得可重复的错误。 他们不只是运行线程一段时间,希望如果有一个错误,它会巧合发生。
例如象棋将贯穿所有的交织,然后给你一个表示交织的标记string,表示发现了一个死锁,这样你就可以将testing与从死锁angular度来看有趣的特定交织相关联。
我不知道这个工具的确切内部工作,以及它如何将这些标记string映射回代码,你可能正在改变,以解决死锁,但是你有它…我真的很期待这个工具(和Pex)成为VS IDE的一部分。
我看到有人试着用你自己提出的标准unittests来testing它。 testing很慢,迄今为止还没有发现我们公司所面临的一个并发问题。
在经历了许多失败之后,尽pipe我对单位testing非常满意,但我并不认为并发性错误不是单位testing的优势之一。 我通常鼓励分析和审查,赞成以并发为主题的课堂unit testing。 通过系统的总体概述,在许多情况下可能certificate/伪造线索安全的要求。
无论如何,我会喜欢有人给我的东西,可能会指向相反的,所以我密切关注这个问题。
当我最近不得不解决同样的问题时,我这样想; 首先你现有的类有一个责任,那就是提供一些function。 线程安全并不是对象的责任。 如果需要线程安全,则应使用其他对象来提供此function。 但是,如果其他对象提供线程安全性,则它不能是可选的,因为那样你就不能certificate你的代码是线程安全的。 所以这是我如何处理它:
// 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 } }
generics的使用使得可以在testing中使用伪造的锁来validation在创build事务时总是进行locking,并且在处理事务之前不会释放locking。 现在,您还可以validation在调用重要方法时执行的locking。 生产代码的用法应该如下所示:
// 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(); }
通过确保其他人无法创buildImportantImplementation(例如,通过在提供者中创build它并将其设置为私有类),您现在可以certificate您的类是线程安全的,因为无法在没有事务的情况下访问该类,事务总是需要创build时locking,处置时释放。
确保事务处理正确可能会更困难,如果不正确,您可能会在应用程序中看到奇怪的行为。 您可以使用工具作为微软国际象棋(如另一位build议)来寻找这样的事情。 或者你可以让你的提供者实现这个外观,并使它像这样实现:
public void ImportantMethodThatMustBeThreadSafe() { using (ImportantTransaction transaction = CreateTransaction()) { transaction.Facade.ImportantMethodThatMustBeThreadSafe(); } }
即使这是实现,我希望你可以根据需要找出testing来validation这些类。
带有springframeworkstesting模块(或其他扩展)的testNG或Junit具有对并发testing的基本支持。
此链接可能会让你感兴趣
你将不得不为每个需要关注的并发场景构build一个testing用例。 这可能需要用慢速等效(或模拟)代替高效操作,并在循环中运行多个testing,以增加争用的可能性
没有具体的testing用例,就很难提出具体的testing
一些潜在有用的参考材料:
- 奥伦Ellenbogen的博客
- 垂直切片博客
- 可能重复的SO问题
虽然它不像使用Racer或Chess这样的工具,但我已经使用这种东西来testing线程安全:
// 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++; } }
这个理论是,如果你能够在一个非常短的时间内始终如一地导致一个线程危险的条件,那么你应该能够带来线程安全的条件,并且通过等待相当多的时间来validation它们,而不会观察到状态腐败。
我确实承认,这可能是一个原始的方法,可能无助于复杂的情况。
这是我的方法。 这个testing不关注死锁,而是关注一致性。 我正在testing一个带有同步块的方法,代码如下所示:
synchronized(this) { int size = myList.size(); // do something that needs "size" to be correct, // but which will change the size at the end. ... }
产生一个可靠地产生线程冲突的场景是很困难的,但是这就是我所做的。
首先,我的unit testing创build了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()应该能够抛出exception。 但是我们还不能确定这会产生冲突。 所以我们不知道testing是否有效。 所以这里的技巧是: 注释掉你的同步关键字并运行testing。 如果这产生冲突,testing将失败。 如果失败没有同步关键字,您的testing是有效的。
这就是我所做的,我的testing没有失败,所以它还没有成为一个有效的testing。 但是,通过将上面的代码放在一个循环中并连续运行100次,我能够可靠地产生一个失败。 所以我把这个方法叫做5000次。 (是的,这会产生一个慢的testing,不要担心,你的顾客不会被这个困扰,所以你也不应该这样做)
一旦我把这段代码放在一个外层循环中,我能够可靠地看到外层循环的第20次迭代失败。 现在我确信testing是有效的,我恢复了同步的关键字来运行实际的testing。 (有效。)
您可能会发现testing在一台机器上是有效的,而不是在另一台机器上。 如果testing在一台机器上有效,并且您的方法通过了testing,那么在所有机器上都可能是线程安全的。 但是你应该在运行你的夜间unit testing的机器上testing有效性。