Unit test для обеспечения безопасности потоков?

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

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

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

Ответ 1

Есть два продукта, которые могут вам помочь:

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

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

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

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

Ответ 2

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

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

Ответ 3

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

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

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

Ответ 4

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

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

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

Ответ 5

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

// 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 (как предложено в другом anser), чтобы искать такие вещи. Или вы можете заставить своего провайдера реализовать фасад и заставить его реализовать его следующим образом:

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

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

Ответ 6

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

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

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

Ответ 7

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

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

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

Ответ 8

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

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

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

Ответ 9

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

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

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

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

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