我已经编写了一个类和许多单元测试,但是我没有使其线程安全。现在,我想要使这个类线程安全,但为了证明它并使用TDD,我想在开始重构之前编写一些失败的单元测试。
有什么好办法吗?
我的第一个想法是创建几个线程,并使它们以不安全的方式使用该类。用足够多的线程进行足够多的次数操作,我就有可能看到它崩溃。
我已经编写了一个类和许多单元测试,但是我没有使其线程安全。现在,我想要使这个类线程安全,但为了证明它并使用TDD,我想在开始重构之前编写一些失败的单元测试。
有什么好办法吗?
我的第一个想法是创建几个线程,并使它们以不安全的方式使用该类。用足够多的线程进行足够多的次数操作,我就有可能看到它崩溃。
有两个产品可以帮助你:
使用这两种工具很简单-编写一个简单的单元测试,运行你的代码多次并检查你的代码是否存在死锁/竞争条件。
编辑:
谷歌发布了一款名为ThreadSanitizer的工具,它可以在运行时(而非测试期间)检查竞争条件。
它可能无法找到所有竞争条件,因为它只分析当前运行而非所有可能的情况,但是一旦发生竞争条件,该工具可能会帮助你找到它。
更新: 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等工具(如另一个答案中建议的)来查找此类问题。或者您可以让提供程序实现facade并使其像这样实现:
public void ImportantMethodThatMustBeThreadSafe()
{
using (ImportantTransaction transaction = CreateTransaction())
{
transaction.Facade.ImportantMethodThatMustBeThreadSafe();
}
}
虽然这是我希望你能实现的代码,但你需要自己设计测试用例以验证这些类是否符合要求。
虽然这种方法不像使用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++;
}
}
理论上,如果您可以在非常短的时间内一致地引发线程危险条件,那么您应该能够通过等待相对较长的时间而不观察状态损坏来实现线程安全条件并进行验证。
我承认这可能是一个原始的方法,并且在复杂情况下可能没有帮助。
使用Spring Framework的测试模块(或其他扩展)进行测试时,testNG或Junit都具有基本的并发测试支持。
这个链接可能会对你有所帮助。
你需要为每个并发场景构建一个测试用例;这可能需要用较慢的等效操作(或模拟)替换高效操作,并在循环中运行多个测试,以增加争用的机会。
没有具体的测试用例,很难提出具体的测试方案。
一些可能有用的参考资料:
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次迭代时可靠地看到故障。现在我有信心测试是有效的,并恢复了同步关键字以运行实际测试。(它成功了。)
您可能会发现测试在一台机器上是有效的,在另一台机器上却无效。如果测试在一台机器上有效,并且您的方法通过了测试,则可以认为它在所有机器上都是线程安全的。但是您应该在运行夜间单元测试的机器上进行有效性测试。