简体   繁体   English

线程安全的单元测试?

[英]Unit test for thread safe-ness?

I've written a class and many unit test, but I did not make it thread safe. 我写了一个类和许多单元测试,但我没有让它线程安全。 Now, I want to make the class thread safe, but to prove it and use TDD, I want to write some failing unit tests before I start refactoring. 现在,我想让类线程安全,但为了证明它并使用TDD,我想在开始重构之前编写一些失败的单元测试。

Any good way to do this? 有什么好办法吗?

My first thought is just create a couple threads and make them all use the class in an unsafe way. 我的第一个想法就是创建一些线程并使它们以不安全的方式使用该类。 Do this enough times with enough threads and I'm bound to see it break. 用足够的线程做足够的时间,我一定会看到它破裂。

There are two products that can help you there: 有两种产品可以帮助您:

Both check for deadlocks in your code (via unit test) and I think Chess checks for race conditions as well. 两者都检查代码中的死锁(通过单元测试),我认为Chess也会检查竞争条件。

Using both tools is easy - you write a simple unit test and the run your code several times and check if deadlocks/race conditions is possible in your code. 使用这两种工具很简单 - 您可以编写一个简单的单元测试并多次运行代码,并检查代码中是否存在死锁/竞争条件。

Edit: Google has released a tool that checks for race condition in runtime (not during tests) that called thread-race-test . 编辑:谷歌发布了一个工具,用于检查调用线程竞争测试的运行时(而不是测试期间)的竞争条件。
it won't find all of the race conditions because it only analyse the current run and not all of the possible scenarios like the tool above but it might help you find the race condition once it happens. 它不会找到所有的竞争条件,因为它只分析当前的运行而不是像上面的工具那样的所有可能的情况,但它可能会帮助你找到竞争条件一旦发生。

Update: Typemock site no longer had a link to Racer, and it was not been updated in the last 4 years. 更新: Typemock站点不再有到Racer的链接,并且在过去的4年中没有更新。 I guess the project was closed. 我猜这个项目已经关闭了。

The problem is that most of the multi-threading issues, like race conditions, are non-deterministic by their nature. 问题在于,大多数多线程问题,如竞争条件,本质上都是不确定的。 They can depend on hardware behavior which you can't possibly emulate or trigger. 它们可以依赖于您无法模拟或触发的硬件行为。

That means, even if you make tests with multiple threads, they will not be consistently failing if you have a defect in your code. 这意味着,即使您使用多个线程进行测试,如果您的代码中存在缺陷,它们也不会始终失败。

Note that Dror's answer does not explicitly say this, but at least Chess (and probably Racer) work by running a set of threads through all their possible interleavings to get repreoducible errors. 请注意,Dror的答案没有明确说明这一点,但至少Chess(可能还有Racer)通过在所有可能的交错中运行一组线程来获得可重现的错误。 They do not just run the threads for a while hoping that if there is an error it will happen by coincidence. 他们不只是运行线程一段时间,希望如果有错误,它将发生巧合。

Chess for example will run through all interleavings and then give you a tag string that represents the interleaving that a deadlock was found on so that you can attribute your tests with the specific interleavings that are interesting from a deadlocking perspective. 例如,Chess将遍历所有交错,然后为您提供一个标记字符串,表示发现死锁的交错,以便您可以使用从死锁视角中感兴趣的特定交错来归因您的测试。

I do not know the exact internal workings of this tool, and how it maps these tag strings back to code that you may be changing to fix a deadlock, but there you have it... I am actually really looking forward to this tool (and Pex) becoming part of the VS IDE. 我不知道这个工具的确切内部工作方式,以及它如何将这些标记字符串映射回您可能正在修改以修复死锁的代码,但是您有它...我实际上真的很期待这个工具(和Pex)成为VS IDE的一部分。

I have seen people try to test this with standard unittests as you yourself propose. 我看到有人试图用你自己提出的标准单元测试来测试它。 The tests are slow, and have so far failed to identify a single of the concurrency problems our company struggles with. 测试速度很慢,到目前为止还未能确定我们公司遇到的一个并发问题。

After many failures, and despite my love for unittests, I have come to accept that errors in concurrency is not one of unittests strengths. 经过多次失败,尽管我对单元测试很感兴趣,但我已经开始接受并发性错误并不是单元测试的优势之一。 I usually encourage analysis and review in favour of unittests for classes where concurrency is a subject. 我通常鼓励分析和审查,以支持并发是主题的类的单元测试。 With total overview of the system it is in many cases possible to prove/falsify claims of thread safety. 通过对系统的总体概述,在许多情况下可以证明/伪造线程安全性声明。

Anyhow I would love for someone to give me something that might point to the opposite, so I watch this question closely. 无论如何,我希望有人给我一些可能指向相反的东西,所以我仔细观察这个问题。

When I recently had to address the same problem I thought of it this way; 当我最近不得不解决同样的问题时,我就这样想了; First of all your existing class has one responsibility and that is to provide some functionality. 首先,您现有的类有一个责任,那就是提供一些功能。 It is not the objects responsibility to be thread safe. 线程安全不是对象的责任。 If it needs to be thread safe some other object should be used to provide this functionality. 如果需要线程安全,则应使用其他一些对象来提供此功能。 But if some other object is providing the thread safe-ness it cannot be optional because then you cannot prove your code is thread safe. 但是,如果某个其他对象提供线程安全,则它不能是可选的,因为那时您无法证明您的代码是线程安全的。 So this is how I handle it: 这就是我处理它的方式:

// 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
    }
}

The use of generics makes it possible to use a fake lock in your tests to verify that the lock is always taken when a transaction is created and not released until transaction is disposed. 泛型的使用使得可以在测试中使用假锁来验证在创建事务时始终采用锁定,而在事务处理之前不会释放锁定。 Now you can also verify that the lock is taken when your important method is called. 现在,您还可以在调用重要方法时验证是否已执行锁定。 Usage in production code should look something like this: 生产代码中的用法应如下所示:

// 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();
}

By making sure the ImportantImplementation cannot be created by somebody else (by for example create it in the provider and make it a private class) you kan now prove your class is thread safe since it cannot be accessed without a transaction and the transaction always takes the lock when created and releases it when disposed. 通过确保其他人无法创建ImportantImplementation(例如在提供程序中创建它并使其成为私有类),您现在可以证明您的类是线程安全的,因为没有事务就无法访问它,并且事务总是需要创建时锁定,处理时释放它。

Make sure the transaction is disposed correctly can be harder and if not you might see weird behaviour in your application. 确保事务处理正确可能更难,如果不是,您可能会在应用程序中看到奇怪的行为。 You can use tools as Microsoft Chess (as suggested in another anser) to look for things like that. 您可以使用工具作为Microsoft Chess(如另一个anser中所建议的)来查找类似的东西。 Or you can have your provider implement the facade and make it implement it like this: 或者您可以让您的提供程序实现外观并使其实现如下:

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

Even though this is the implementation I hope you can figure out the tests to verify these classes as needed. 即使这是实现,我希望您可以根据需要找出测试来验证这些类。

testNG or Junit with springframeworks test module (or other extension) has basic support for concurrency testing. 带有springframeworks测试模块(或其他扩展)的testNG或Junit对并发测试有基本支持。

This link might interest you 此链接可能会让您感兴趣

http://www.cs.rice.edu/~javaplt/papers/pppj2009.pdf http://www.cs.rice.edu/~javaplt/papers/pppj2009.pdf

you'll have to construct a test case for each concurrency scenario of concern; 你将不得不为每个关注的并发场景构建一个测试用例; this may require replacing efficient operations with slower equivalents (or mocks) and running multiple tests in loops, to increase the chance of contentions 这可能需要用较慢的等效(或模拟)替换有效的操作并在循环中运行多个测试,以增加争用的机会

without specific test cases, it is difficult to propose specific tests 没有具体的测试用例,很难提出具体的测试

some potentially useful reference material: 一些可能有用的参考资料:

Though it's not as elegant as using a tool like Racer or Chess, I have used this sort of thing for testing for thread safety: 虽然它不像使用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++;
    }
}

The theory is that if you can cause a thread dangerous condition consistently to occur in a very short amount of time, you should be able to bring about thread safe conditions and verify them by waiting a relatively large amount of time without observing state corruption. 理论上说,如果你可以在很短的时间内导致线程危险情况持续发生,你应该能够带来线程安全条件并通过等待相对较长的时间来验证它们,而不会观察到状态损坏。

I do admit that this may be a primitive way of going about it and may not help in complex scenarios. 我承认这可能是一种原始的方式,在复杂的情况下可能无济于事。

Here's my approach. 这是我的方法。 This test is not concerned with deadlocks, it's concerned with consistency. 这个测试不关心死锁,而是关注一致性。 I'm testing a method with a synchronized block, with code that looks something like this: 我正在使用synchronized块测试方法,代码看起来像这样:

synchronized(this) {
  int size = myList.size();
  // do something that needs "size" to be correct,
  // but which will change the size at the end.
  ...
}

It's tough to produce a scenario that will reliably produce a thread conflict, but here's what I did. 很难产生可靠地产生线程冲突的场景,但这就是我所做的。

First, my unit test created 50 threads, launched them all at the same time, and had them all call my method. 首先,我的单元测试创​​建了50个线程,同时启动它们,并让它们都调用我的方法。 I use a CountDown Latch to start them all at the same time: 我使用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.

This may or may not produce a conflict. 这可能会也可能不会产生冲突。 My testMethod() should be capable of throwing an exception if a conflict occurs. 如果发生冲突,我的testMethod()应​​该能够抛出异常。 But we can't yet be sure that this will generate a conflict. 但我们还不能确定这会产生冲突。 So we don't know if the test is valid. 所以我们不知道测试是否有效。 So here's the trick: Comment out your synchronized keyword(s) and run the test. 所以这就是诀窍: 注释掉你的synchronized关键字并运行测试。 If this produces a conflict, the test will fail. 如果这会产生冲突,则测试将失败。 If it fails without the synchronized keyword, your test is valid. 如果在没有synchronized关键字的情况下失败,则测试有效。

That's what I did, and my test didn't fail, so it was not (yet) a valid test. 这就是我所做的,我的测试没有失败,所以它还没有(有效)测试。 But I was able to reliably produce a failure by putting the code above inside a loop, and running it 100 times consecutively. 但是我能够通过将上面的代码放在循环中并连续运行100次来可靠地产生故障。 So I call the method 5000 times. 所以我称这个方法5000次。 (Yes, this will produce a slow test. Don't worry about it. Your customers won't be bothered by this, so you shouldn't either.) (是的,这会产生一个缓慢的测试。不要担心。你的客户不会为此烦恼,所以你不应该。)

Once I put this code inside an outer loop, I was able to reliably see a failure on about the 20th iteration of the outer loop. 一旦我把这个代码放在外部循环中,我就可以可靠地看到外循环的第20次迭代失败了。 Now I was confident the test was valid, and I restored the synchronized keywords to run the actual test. 现在我确信测试有效,并且我恢复了synchronized关键字以运行实际测试。 (It worked.) (有效。)

You may discover that the test is valid on one machine and not on another. 您可能会发现测试在一台计算机上有效,而在另一台计算机上无效。 If the test is valid on one machine and your methods pass the test, then it's presumably thread-safe on all machines. 如果测试在一台机器上有效并且您的方法通过了测试,那么它可能在所有机器上都是线程安全的。 But you should test for validity on the machine that runs your nightly unit tests. 但是你应该在运行夜间单元测试的机器上测试有效性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM