线程安全的单元测试?

发布于 2024-08-11 14:09:08 字数 173 浏览 6 评论 0原文

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

有什么好的方法可以做到这一点吗?

我的第一个想法是创建几个线程并让它们都以不安全的方式使用该类。用足够多的线程执行足够多次,我一定会看到它破裂。

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.

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

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

两者都会检查代码中的死锁(通过单元测试),我认为 Chess 会检查比赛条件也是如此。

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

编辑:
Google 发布了一个工具,可以在运行时(而不是测试期间)检查竞争条件,名为 线程竞赛测试
它不会找到所有的竞争条件,因为它只分析当前的运行,而不是像上面的工具那样分析所有可能的场景,但一旦发生竞争条件,它可能会帮助您找到竞争条件。

更新:
Typemock 网站不再有 Racer 的链接,并且在过去 4 年里没有更新。我猜这个项目已经关闭了。

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.

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. I guess the project was closed.

笑红尘 2024-08-18 14:09:08

问题在于,大多数多线程问题(例如竞争条件)本质上是不确定的。它们可能依赖于您无法模拟或触发的硬件行为。

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

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.

百思不得你姐 2024-08-18 14:09:08

请注意,Dror 的答案并没有明确说明这一点,但至少 Chess(可能还有 Racer)通过在所有可能的交错中运行一组线程来获得可重复的错误来工作。他们不只是运行线程一段时间,希望如果出现错误,它会巧合发生。

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

我不知道这个工具的确切内部工作原理,以及它如何将这些标记字符串映射回您可能要更改以修复死锁的代码,但是您已经知道了......我实际上非常期待这个工具(和 Pex)成为 VS IDE 的一部分。

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. 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.

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.

无语# 2024-08-18 14:09:08

我看到人们尝试按照您自己的建议使用标准单元测试来测试这一点。测试速度很慢,而且到目前为止还未能识别出我们公司所面临的任何并发问题。

经过多次失败后,尽管我热爱单元测试,我还是开始接受并发错误并不是单元测试的优势之一。我通常鼓励对以并发为主题的类进行分析和审查,以支持单元测试。通过对系统的全面概述,在许多情况下可以证明/证伪线程安全的声明。

无论如何,我希望有人能给我一些可能指向相反的东西,所以我密切关注这个问题。

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.

同尘 2024-08-18 14:09:08

当我最近不得不解决同样的问题时,我是这样想的;首先,您现有的类有一个职责,那就是提供一些功能。线程安全不是对象的责任。如果需要线程安全,则应使用其他对象来提供此功能。但是,如果其他对象提供线程安全性,那么它就不能是可选的,因为这样您就无法证明您的代码是线程安全的。这就是我处理它的方式:

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

即使这是实现,我希望您能够找出测试来根据需要验证这些类。

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.

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. 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.

滿滿的愛 2024-08-18 14:09:08

testNG或带有springframeworks测试模块(或其他扩展)的Junit对并发测试有基本的支持。

您可能对此链接感兴趣

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

testNG or Junit with springframeworks test module (or other extension) has basic support for concurrency testing.

This link might interest you

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

泪之魂 2024-08-18 14:09:08

您必须为每个关注的并发场景构建一个测试用例;这可能需要用较慢的等价物(或模拟)替换有效的操作,并在循环中运行多个测试,以增加在没有特定测试用例的情况下发生争用的机会

,很难提出特定测试

一些可能有用的参考材料:

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:

一个人的夜不怕黑 2024-08-18 14:09:08

虽然它不像使用 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++;
    }
}

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

我确实承认这可能是一种原始的处理方式,并且在复杂的情况下可能没有帮助。

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:

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

故事未完 2024-08-18 14:09:08

这是我的方法。该测试不关心死锁,它关心一致性。我正在使用同步块测试一个方法,其代码如下所示:

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

很难生成一个能够可靠地产生线程冲突的场景,但这就是我所做的。

首先,我的单元测试创​​建了 50 个线程,同时启动它们,并让它们全部调用我的方法。我使用倒计时锁存器同时启动它们:

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() 应该能够抛出异常。但我们还不能确定这是否会产生冲突。所以我们不知道测试是否有效。因此,技巧如下:注释掉您的同步关键字并运行测试。如果这会产生冲突,则测试将失败。 如果没有使用synchronized关键字就失败了,那么你的测试是有效的。

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

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

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

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(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. I use a CountDown Latch to start them all at the same time:

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. 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. If this produces a conflict, the test will fail. If it fails without the synchronized keyword, your test is valid.

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. So I call the method 5000 times. (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. Now I was confident the test was valid, and I restored the synchronized keywords to run the actual test. (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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文