Должен ли я выполнить модульный тест на проблемы с многопоточностью, прежде чем писать какие-либо блокировки? - PullRequest
8 голосов
/ 27 апреля 2009

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

Этот вопрос относится к .NET

Кто-то спросил код:

public class StackQueue
{
    private Stack<WebRequestInfo> stack = new Stack<WebRequestInfo>();
    private Queue<WebRequestInfo> queue = new Queue<WebRequestInfo>();

    public int Count
    {
        get
        {
            return this.queue.Count + this.stack.Count;
        }
    }

    public void Enqueue(WebRequestInfo requestInfo)
    {
        this.queue.Enqueue(requestInfo);
    }

    public void Push(WebRequestInfo requestInfo)
    {
        this.stack.Push(requestInfo);
    }

    private WebRequestInfo Next()
    {
        if (stack.Count > 0)
        {
            return stack.Pop();
        }
        else if (queue.Count > 0)
        {
            return queue.Dequeue();
        }
        return null;
    }
}

Ответы [ 10 ]

6 голосов
/ 27 апреля 2009

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

Для более серьезной проблемы с обработкой ошибок есть CHESS (в процессе) - возможно, вариант в будущем.

4 голосов
/ 27 апреля 2009

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


Хорошо, теперь, когда вы отправили код:

public int Count
    {
        get
        {
            return this.queue.Count + this.stack.Count;
        }
    }

Это отличный пример того, что у вас возникнут проблемы при написании модульного теста, который выявит проблемы с многопоточностью в вашем коде. Этот код потенциально нуждается в синхронизации, потому что значения this.queue.Count и this.stack.Count могут изменяться в середине вычисления итога, поэтому он может возвращать значение, которое не является «правильным».

ОДНАКО - Учитывая оставшуюся часть определения класса, на самом деле ничто не зависит от того, как Count дает последовательный результат, так что действительно ли это имеет значение, если это «неправильно»? Нет никакого способа узнать это, не зная, как другие классы в вашей программе используют этот. Это делает тестирование на потоки проблем интеграционным тестом, а не модульным тестом.

3 голосов
/ 27 апреля 2009

При написании многопоточного кода вы должны использовать свой мозг даже больше, чем обычно. Вы должны логически рассуждать о каждой отдельной строке кода, независимо от того, является ли она поточно-безопасной или нет. Это все равно, что доказать правильность математической формулы - вы не можете доказать такие вещи, как «N + 1> N для всех N», просто приведя примеры значений N, с которыми формула верна. Точно так же доказательство того, что класс является потокобезопасным, невозможно путем написания тестовых примеров, которые пытаются выявить проблемы с ним. С помощью теста можно только доказать, что есть неисправность, но не то, что неисправностей нет.

Лучшее, что вы можете сделать, - это минимизировать потребность в многопоточном коде. Предпочтительно, чтобы приложение не имело многопоточного кода (например, полагаясь на поточно-ориентированные библиотеки и подходящие шаблоны проектирования), или оно должно быть ограничено очень маленькой областью. Ваш StackQueue класс выглядит достаточно простым, так что вы можете сделать его безопасным для многопотоковой работы, немного подумав.

Предполагая, что реализации Stack и Queue являются поточно-ориентированными (я не знаю библиотеки .NET), вам просто нужно сделать Next() поточно-ориентированным. Count уже является поточно-ориентированным, поскольку ни один клиент не может безопасно использовать возвращаемое им значение без использования клиентской блокировки - зависимости состояния между методами в противном случае нарушали бы код.

Next() не является поточно-ориентированным, поскольку имеет зависимости состояния между методами. Если потоки T1 и T2 вызывают stack.Count одновременно, и он возвращает 1, то один из них получит значение с stack.Pop(), а другой вызовет stack.Pop(), когда стек пуст (который, как представляется, выбрасывает InvalidOperationException). Вам понадобится стек и очередь с неблокирующими версиями Pop() и Dequeue() (те, которые возвращают ноль, когда пусты). Тогда код будет потокобезопасным при написании так:

private WebRequestInfo Next()
{
    WebRequestInfo next = stack.PopOrNull()
    if (next == null)
    {
        next = queue.DequeueOrNull();
    }
    return next;
}
2 голосов
/ 27 апреля 2009

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

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

2 голосов
/ 27 апреля 2009

Многопоточность может привести к таким сложным проблемам, что практически невозможно написать для них модульные тесты. Возможно, вам удастся написать модульный тест со 100% частотой отказов при выполнении в коде, но после того, как вы его пройдете, вполне вероятно, что в коде все еще есть условия гонки и похожие проблемы.

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

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

1 голос
/ 28 апреля 2009

Я согласен с другими авторами, что многопоточного кода следует избегать или, по крайней мере, ограничивать небольшими частями вашего приложения. Тем не менее, я все еще хотел какой-то способ проверить эти маленькие порции. Я работаю над портом .NET библиотеки Java MultithreadedTC . Мой порт называется Ticking Test , а исходный код опубликован в Google Code.

MultithreadedTC позволяет вам написать тестовый класс с несколькими методами, помеченными атрибутом TestThread. Каждый поток может ожидать поступления определенного числа тиков или утверждать, каким он считает текущий счет тиков. Когда все текущие потоки заблокированы, поток-координатор увеличивает счетчик тиков и активирует все потоки, ожидающие следующего счетчика тиков. Если вам интересно, посмотрите MultithreadedTC обзор для примеров. MultithreadedTC был написан теми же людьми, которые написали FindBugs .

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

1 голос
/ 27 апреля 2009

Особенно с многоядерными системами, вы обычно можете тестировать на наличие потоков, но не детерминистически.

Обычно я делаю это, раскручивая несколько потоков, которые просматривают код и подсчитывают количество неожиданных результатов. Эти потоки выполняются в течение некоторого короткого периода времени (обычно 2-3 секунды), затем используют Interlocked.CompareExchange, чтобы добавить свои результаты, и нормально завершите работу. Мой тест, который раскрутил их, затем вызывает .Join для каждого, а затем проверяет, было ли количество ошибок равным 0.

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

1 голос
/ 27 апреля 2009

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

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

Не могли бы вы опубликовать более конкретную информацию о вашем классе?

0 голосов
/ 01 июня 2011
  • Вы находите свой «инвариант» - то, что должно оставаться истинным независимо от количества и чередования потоков клиентов. Это сложная часть.
  • Затем напишите тест, который порождает несколько потоков, применяет метод и утверждает, что инвариант все еще сохраняется. Это не удастся - потому что не существует кода, чтобы сделать его потокобезопасным. Красный
  • Добавьте код, чтобы сделать его поточно-ориентированным и пройти стресс-тестирование. Зеленый. Рефакторинг по мере необходимости.

Подробнее см. В книге ГСНО главы, посвященные многопоточному коду.

0 голосов
/ 27 апреля 2009

Большинство модульных тестов работают последовательно, а не одновременно, поэтому они не выявят проблем с параллелизмом.

Проверка кода программистами с опытом параллелизма - ваш лучший выбор.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...