Модульный тест для безопасности потока? - PullRequest
66 голосов
/ 11 ноября 2009

Я написал класс и много модульных тестов, но я не сделал это потокобезопасным. Теперь я хочу сделать поток класса безопасным, но чтобы доказать это и использовать TDD, я хочу написать несколько неудачных модульных тестов, прежде чем начать рефакторинг.

Есть ли хороший способ сделать это?

Моя первая мысль - просто создать пару потоков и заставить их всех использовать класс небезопасным способом. Делайте это достаточно раз с достаточным количеством нитей, и я обязательно увижу это.

Ответы [ 9 ]

21 голосов
/ 11 ноября 2009

Вам могут помочь два продукта:

Обе проверки на наличие взаимоблокировок в вашем коде (с помощью модульного теста), и я думаю, что шахматы также проверяют условия гонки.

Использовать оба инструмента легко - вы пишете простой модульный тест, запускаете свой код несколько раз и проверяете, возможны ли в вашем коде взаимоблокировки / условия гонки.

Edit: Google выпустил инструмент, который проверяет состояние гонки во время выполнения (не во время тестов), который называется thread-race-test .
он не найдет все условия гонки, потому что он анализирует только текущий пробег, а не все возможные сценарии, подобные приведенному выше инструменту, но может помочь вам найти условие гонки, как только это произойдет.

Обновление: На сайте Typemock больше не было ссылки на Racer, и он не обновлялся последние 4 года. Я предполагаю, что проект был закрыт.

10 голосов
/ 11 ноября 2009

Проблема в том, что большинство проблем многопоточности, таких как условия гонки, по своей природе недетерминированы. Они могут зависеть от поведения оборудования, которое вы не можете эмулировать или вызвать.

Это означает, что даже если вы выполняете тесты с несколькими потоками, они не будут постоянно давать сбой, если у вас есть дефект в коде.

5 голосов
/ 19 ноября 2009

Обратите внимание, что в ответе Дрора это явно не сказано, но, по крайней мере, шахматы (и, возможно, Racer) работают, пропуская набор потоков через все их возможные чередования, чтобы получить недопустимые ошибки. Они не просто запускают потоки некоторое время, надеясь, что в случае ошибки это произойдет по совпадению.

Например, шахматы будут проходить через все чередования, а затем предоставят вам строку тега, представляющую чередование, в котором обнаружен тупик, чтобы вы могли приписать свои тесты определенным чередованиям, которые интересны с точки зрения взаимоблокировки.

Я не знаю точную внутреннюю работу этого инструмента и как он отображает эти строки тегов обратно в код, который вы, возможно, изменяете, чтобы исправить тупик, но там у вас есть ... Я действительно очень жду этот инструмент (и Pex) становятся частью VS IDE.

3 голосов
/ 11 ноября 2009

Я видел, как люди пытались проверить это с помощью стандартных юнит-тестов, как вы сами предлагаете. Тесты медленные, и до сих пор не удалось выявить ни одной из проблем параллелизма, с которыми сталкивается наша компания.

После многих неудач и несмотря на мою любовь к юнит-тестам, я пришел к выводу, что ошибки в параллелизме не являются одной из сильных сторон юнит-тестов. Я обычно поощряю анализ и обзор в пользу юнит-тестов для классов, где параллелизм является предметом. С полным обзором системы во многих случаях можно доказать / сфальсифицировать утверждения о безопасности потоков.

В любом случае, я бы хотел, чтобы кто-то дал мне что-то, что могло бы указывать на противоположное, поэтому я внимательно слежу за этим вопросом.

2 голосов
/ 17 ноября 2009

Когда мне недавно пришлось решать ту же проблему, я думал об этом таким образом; Прежде всего, у вашего существующего класса есть одна обязанность, а именно предоставить некоторую функциональность. Быть потокобезопасным не является обязанностью объектов. Если он должен быть потокобезопасным, для обеспечения этой функциональности следует использовать какой-то другой объект. Но если какой-то другой объект обеспечивает безопасность потока, он не может быть необязательным, потому что тогда вы не сможете доказать, что ваш код является потокобезопасным. Вот как я справляюсь:

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

Несмотря на то, что это реализация, я надеюсь, что вы сможете найти тесты для проверки этих классов по мере необходимости.

1 голос
/ 15 февраля 2012

Хотя это не так элегантно, как использование таких инструментов, как 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++;
    }
}

Теория состоит в том, что, если вы можете вызвать постоянное возникновение опасного состояния потока в очень короткий промежуток времени, вы должны быть в состоянии создать безопасные условия потока и проверить их, ожидая относительно большое количество времени без наблюдения за состоянием коррупция.

Я признаю, что это может быть примитивным способом решения этой проблемы и может не помочь в сложных сценариях.

1 голос
/ 11 ноября 2009

вам нужно будет создать контрольный пример для каждого сценария параллелизма; это может потребовать замены эффективных операций более медленными эквивалентами (или имитациями) и выполнения нескольких тестов в циклах, чтобы увеличить вероятность разногласий

без конкретных тестовых случаев, трудно предложить конкретные тесты

некоторые потенциально полезные справочные материалы:

1 голос
/ 11 ноября 2009

testNG или Junit с тестовым модулем springframeworks (или другим расширением) имеет базовую поддержку параллельного тестирования.

Эта ссылка может вас заинтересовать

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

0 голосов
/ 08 февраля 2013

Вот мой подход. Этот тест не связан с тупиками, он связан с согласованностью. Я тестирую метод с синхронизированным блоком, с кодом, который выглядит примерно так:

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-й итерации внешнего цикла. Теперь я был уверен, что тест был действительным, и восстановил синхронизированные ключевые слова, чтобы запустить реальный тест. (Это сработало.)

Вы можете обнаружить, что тест действителен на одном компьютере, а не на другом. Если тест действителен на одной машине, и ваши методы проходят его, то он предположительно является поточно-ориентированным на всех машинах. Но вы должны проверить правильность на компьютере, на котором выполняются ваши ночные юнит-тесты.

...