Подход к тестированию для многопоточного программного обеспечения

У меня есть часть сформировавшегося геопространственного программного обеспечения, которому недавно переписали области, чтобы воспользоваться лучшим преимуществом нескольких процессоров, доступных в современных ПК. А именно, от дисплея, GUI, пространственного поиска и основной обработки все отделились для разделения потоков. Программное обеспечение имеет довольно большой комплект автоматизации GUI для функциональной регрессии и другого меньший для регрессии производительности. В то время как все автоматизированные тесты являются передающими, я не убежден, что они предоставляют почти достаточно страховой защиты с точки зрения нахождения ошибок, связывающих условия состязания, мертвые блокировки и другой nasties, связанный с многопоточностью. Какие методы Вы использовали бы, чтобы видеть, существуют ли такие ошибки? Какие методы Вы защитили бы для выкорчевывания их, предположив, что существуют некоторые там для выкорчевывания?

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

Приложением является C++, MFC, mulitple документ/представление, со многими потоками на документ. Механизм блокировки, который я использую, основан на объекте, который включает указатель на CMutex, который заблокирован в ctor и освобожден в dtor. Я использую локальные переменные этого объекта заблокировать различные биты кода как требуется, и мое взаимное исключение имеет время, которое запускает мой предупреждение, если тайм-аут достигнут. Я стараюсь не блокировать, если это возможно, с помощью копий ресурса, если это возможно, вместо этого.

Что другие тесты Вы выполнили бы?

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

11
задан SmacL 13 March 2010 в 08:48
поделиться

6 ответов

Во-первых, большое спасибо за ответы. Ответы, размещенные на разных форумах, см. на:

http://www.sqaforums.com/showflat.php?Cat=0&Number=617621&an=0&page=0#Post617621

Подход к тестированию многопоточного программного обеспечения

http://www.softwaretestingclub.com/forum/topics/testing-approach-for?xg_source=activity

и в следующем списке рассылки: software-testing@yahoogroups.com

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

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

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

  • Добавление тестовой опции, которая позволяла считывать тайм-ауты мьютекса из конфигурационного файла, который, в свою очередь, мог управляться моей автоматикой.

  • Увеличение тайм-аутов мьютекса за пределы типичного времени, ожидаемого для выполнения участка кода потока, и выдача отладочного исключения по тайм-ауту.

  • Запуск автоматизации в сочетании с отладчиком (VS2008), чтобы при возникновении проблемы было больше шансов ее отследить.

  • Запуск без отладчика, чтобы убедиться, что отладчик не скрывает другие ошибки, связанные с таймингом.

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

Тип обнаруженных ошибок, как правило, был серьезным по своей природе, например, разыменование недопустимых указателей, и даже под отладчиком потребовалось довольно много времени на их отслеживание. Как уже обсуждалось в другом месте, функции SuspendThread и ResumeThread оказались основными виновниками, и все использование этих функций было заменено мьютексами. Аналогично все критические секции были удалены из-за отсутствия тайм-аутов. Закрытие документов и выход из программы также были источником ошибок, когда в одном случае документ был уничтожен, а рабочий поток оставался активным. Для решения этой проблемы был добавлен единственный мьютекс для каждого потока, чтобы контролировать жизнь потока, и его получал деструктор документа, чтобы убедиться, что поток завершился, как ожидалось.

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

1
ответ дан 3 December 2019 в 04:52
поделиться

Как упоминает Лен Холгейт, я бы предложил провести рефакторинг (при необходимости) и создать интерфейсы для частей кода, где разные потоки взаимодействуют с объектами, несущими состояние. Затем эти части кода можно протестировать отдельно от кода, содержащего фактическую функциональность. Чтобы проверить такой модульный тест, я бы подумал об использовании инструмента покрытия кода (для этого я использую gcov и lcov), чтобы убедиться, что все в поточно-безопасном интерфейсе покрыто.

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

1
ответ дан 3 December 2019 в 04:52
поделиться

Некоторые предложения:

  • Используйте закон больших чисел и выполняйте тестируемую операцию не только один раз, но и несколько раз. раз.
  • Проведите стресс-тест вашего кода, преувеличивая сценарии. Например. для тестирования вашего класса, содержащего мьютекс, используйте сценарии, в которых код, защищенный мьютексом:
    • очень короткий и быстрый (одна инструкция)
    • занимает много времени (спите с большое значение)
    • содержит явные переключатели контекста (Sleep (0))
  • Запустите тест на различных архитектурах. (Даже если ваше программное обеспечение предназначено только для Windows, протестируйте его на одно- и многоядерных процессорах с гиперпоточностью и без нее, а также в широком диапазоне тактовых частот)
  • Постарайтесь разработать свой код таким образом, чтобы большая часть его не подвергалась проблемам с многопоточностью . Например. вместо доступа к совместно используемым данным (что требует блокировки или очень тщательно разработанных методов предотвращения блокировок) позвольте вашим рабочим потокам работать с копиями данных и связываться с ними с помощью очередей.Затем вам нужно только проверить свой класс очереди на безопасность потоков.
  • Запускайте тесты, когда система простаивает, а также когда она находится под нагрузкой из-за других задач (например, наш сервер сборки часто запускает несколько сборок параллельно. Это само по себе показало много ошибок, связанных с многопоточностью, которые возникали, когда система находилась под нагрузкой.)
  • Избегайте установки таймаутов. Если такое утверждение не удается, вы не знаете, поврежден ли код или слишком короткое время ожидания. Вместо этого используйте очень большой тайм-аут (чтобы убедиться, что тест в конечном итоге не сработает). Если вы хотите проверить, что операция не занимает больше определенного времени, измерьте продолжительность, но не используйте для этого тайм-аут.
10
ответ дан 3 December 2019 в 04:52
поделиться

Не совсем ответ:

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

  • Тип процессора
  • Скорость процессора
  • Количество процессоров/ядер
  • Уровень оптимизации
  • Работа внутри или вне отладчика
  • Операционная система

Наверняка есть еще предпосылки, о которых я забыл.

Поскольку MT-баги так сильно зависят от точного времени выполнения кода, здесь вступает в силу "принцип неопределенности" Гейзенберга: Если вы хотите протестировать MT-баг, вы изменяете время выполнения кода своими "мерами", которые могут предотвратить появление бага...

Временные параметры - это то, что делает MT-баги настолько недетерминированными. Другими словами: У вас может быть программа, которая работает месяцами, а затем в какой-то день происходит сбой, и после этого может работать годами. Если у вас нет отладочных журналов/ дампов ядра и т.д., вы можете никогда не узнать, почему она падает.

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

Для наглядности приведу пример (упрощенный) из реальной жизни (я столкнулся с этим, когда менял работодателя и смотрел на существующий там код):

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

class A {
  private:
    int refcount;
  public:
    A() : refcount(0) {
    }
    void Ref() {
      refcount++;
    }
    void Release() {
      refcount--;
      if (refcount == 0) {
        delete this;
      }
    }
};

Это выглядит довольно просто и не о чем беспокоиться. Но это не потокобезопасно! Потому что "refcount++" и "refcount--" - это не атомарные операции, а три операции:

  • чтение refcount из памяти в регистр
  • инкремент/уменьшение регистра
  • запись refcount из регистра в память

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

  • Поток A: прочитать счетчик из памяти в регистр (счетчик: 8)
  • Поток A: увеличить регистр
    • CONTEXT CHANGE -
  • Поток B: прочитать счетчик из памяти в регистр (счетчик: 8)
  • Thread B: increment register
  • Thread B: write refcount from register to memory (refcount: 9)
    • CONTEXT CHANGE -
  • Thread A: write refcount from register to memory (refcount: 9)

В результате получается: refcount = 9, а должно быть 10!

Это можно решить только с помощью атомарных операций (т.е. InterlockedIncrement() & InterlockedDecrement() в Windows).

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

Но это может произойти! (Вероятность возрастает, если у вас многопроцессорная или многоядерная система, потому что для этого не нужно переключать контекст). Это произойдет через несколько дней, недель или месяцев!

2
ответ дан 3 December 2019 в 04:52
поделиться

Похоже, вы используете инструменты Microsoft. В Microsoft Research есть группа, которая работает над инструментом, специально разработанным для устранения ошибок параллелизма. Проверьте ШАХМАТЫ . Другие исследовательские проекты, находящиеся на ранних стадиях, - это Cuzz и Featherlite .

VS2010 включает очень красивый профилировщик параллелизма, видео доступно здесь.

2
ответ дан 3 December 2019 в 04:52
поделиться

Хотя я согласен с ответом @rstevens в том, что в настоящее время не существует способа юнит-тестирования потоковых проблем со 100% уверенностью, есть некоторые вещи, которые я нашел полезными.

Во-первых, какие бы тесты вы ни проводили, убедитесь, что вы запускаете их на множестве разных машин. У меня есть несколько машин для сборки, все они разные, многоядерные, одноядерные, быстрые, медленные и т.д. Их разнообразие хорошо тем, что на разных машинах возникают разные проблемы с потоками. Я регулярно удивлялся, когда добавлял новую машину в свою ферму и внезапно обнаруживал новую ошибку потоковой обработки; и я говорю о том, что новая ошибка обнаруживается в коде, который выполнялся 10000 раз на других машинах и который проявляется 1 из 10 на новой...

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

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

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

Теперь вы проводите все эти тесты много раз на разном оборудовании...

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

Пример того, как я тестирую многопоточный C++ код, можно посмотреть здесь: http://www.lenholgate.com/blog/2004/05/practical-testing.html

7
ответ дан 3 December 2019 в 04:52
поделиться
Другие вопросы по тегам:

Похожие вопросы: