我已经编写了一个类和许多单元测试,但没有使它成为线程安全的。现在,我想使类线程安全,但是要证明它并使用TDD,我想在开始重构之前编写一些失败的单元测试。
有什么好办法吗?
我的第一个想法就是创建几个线程,并使它们全部以不安全的方式使用该类。用足够的线程执行此操作足够的时间,我一定会看到它破裂。
我已经编写了一个类和许多单元测试,但没有使它成为线程安全的。现在,我想使类线程安全,但是要证明它并使用TDD,我想在开始重构之前编写一些失败的单元测试。
有什么好办法吗?
我的第一个想法就是创建几个线程,并使它们全部以不安全的方式使用该类。用足够的线程执行此操作足够的时间,我一定会看到它破裂。
Answers:
有两种产品可以为您提供帮助:
两者都检查代码中的死锁(通过单元测试),我认为国际象棋也检查竞争条件。
使用这两种工具都很容易-您编写一个简单的单元测试,然后多次运行代码,并检查代码中是否可能出现死锁/争用条件。
编辑:
Google发布了一个用于在运行时(而不是在测试过程中)检查竞争状况的工具,称为thread-race-test。
它不会找到所有竞争条件,因为它仅分析当前运行情况,而不是分析所有可能的情况,例如上面的工具,但是一旦发生情况,它可能会帮助您找到竞争条件。
更新: Typemock网站不再具有Racer的链接,并且在过去4年中未更新。我想这个项目已经关闭。
当我最近不得不解决相同的问题时,我是这样想的。首先,您现有的类有一个责任,那就是提供一些功能。线程安全不是对象的责任。如果需要线程安全,则应使用其他一些对象来提供此功能。但是,如果某个其他对象提供了线程安全性,那么它就不是可选的,因为那样就无法证明您的代码是线程安全的。所以这就是我的处理方式:
// 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();
}
}
即使这是实现,我希望您可以找出测试以根据需要验证这些类。
带有springframeworks测试模块(或其他扩展)的testNG或Junit对并发测试具有基本支持。
该链接可能会让您感兴趣
您必须为每个关注的并发场景构建一个测试用例;这可能需要用较慢的等效项(或模拟)替换有效的操作,并在循环中运行多个测试,以增加争用的机会
没有特定的测试用例,很难提出特定的测试
一些可能有用的参考资料:
尽管它不如使用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++;
}
}
从理论上讲,如果您可以在很短的时间内导致线程危险状况持续发生,那么您应该能够带来线程安全状况并通过等待相对较长的时间来验证它们,而不会观察到状态损坏。
我确实承认这可能是解决问题的原始方法,并且在复杂的情况下可能无济于事。
这是我的方法。该测试与死锁无关,它与一致性有关。我正在测试带有同步块的方法,其代码如下所示:
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次迭代失败。现在,我确信测试是有效的,并且我恢复了同步关键字以运行实际测试。(有效。)
您可能会发现测试在一台计算机上有效,而在另一台计算机上无效。如果测试在一台机器上有效,并且您的方法通过了测试,那么大概在所有机器上都是线程安全的。但是,您应该在运行夜间单元测试的计算机上测试有效性。