2010-03-13 3 views
9

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

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

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

Какие еще тесты вы бы выполнили?

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

ответ

1

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

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

Testing approach for multi-threaded software

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

и следующий список рассылки; [email protected]

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

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

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

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

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

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

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

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

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

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

2

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

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

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

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

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

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

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

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

Представьте, что вы есть класс. Вы хотите, чтобы этот класс автоматически удалялся, если никто больше его не использует. Итак, вы создаете контрольный счетчик в этом классе: (Я знаю, что это плохой стиль для удаления экземпляра класса в одном из его методов. Это из-за упрощения реального кода, который использует класс 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 из регистра в память

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

  • Тема A: прочитать RefCount из памяти в регистр (RefCount: 8)
  • Тема A: инкремент регистра
    • КОНТЕКСТ ИЗМЕНЕНИЕ -
  • тема B: RefCount чтения из памяти для регистрации (RefCount: 8)
  • резьбы Б: приращение регистра
  • резьбы Б: написать RefCount из регистра в память (RefCount: 9)
    • УСЛОВИЯ ИЗМЕНЕНИЯ -
  • Автор А: написать RefCount из регистра в память (RefCount: 9)

Таким образом, результат: refcount = 9, но должно быть 10!

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

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

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

+0

Даже с заблокированными функциями это все равно не будет безопасным для потоков. Рассмотрим: Тема 1: a-> Ref(); // работа с a-> Release(); Резьба 2: a-> Release(); Предположим, что поток 1 вызывает Ref(), когда поток 2 находится в вызове Release() между InterlockedDecrement и удалением этого ... – oefe

+0

Переопределение параллелизма и команд, которое происходит в CPU, делает ошибки параллелизма еще более непредсказуемыми, чем пример в этой должности. См. Http://www.infoq.com/presentations/click-crash-course-modern-hardware –

+0

@ Эско Луонтола: Правильно. Но если мой простой пример уже вызывает проблемы, то он, конечно, не станет лучше, если компилятор или процессор переупорядочат инструкции :-) – mmmmmmmm

5

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

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

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

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

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

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

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

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

8

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

  • Использовать law of large numbers и выполнить операцию при испытании не только один раз, но много раз ,
  • Стресс-тест вашего кода путем преувеличения сценариев. Например. чтобы проверить свой класс мьютекса удерживающего, использовать сценарии, где мьютекс защищенного код:
    • является очень коротким и быстрым (одна инструкцией)
    • отнимает много времени (Sleep с большим значением)
    • содержит явные контекстные переключатели (Sleep (0))
  • Запустите свой тест на различных архитектурах.(Даже если ваше программное обеспечение только для Windows, протестируйте его на одно- и многоядерных процессорах с гиперпотоком и без него и с широким диапазоном тактовых частот)
  • Попробуйте создать свой код таким образом, чтобы большая часть его не подвергалась многопоточным проблемам , Например. вместо того, чтобы получать доступ к общим данным (что требует блокировки или очень тщательно разработанные методы предотвращения блокировки), пусть ваши рабочие потоки работают с копиями данных и общаются с ними, используя очереди. Затем вам нужно только проверить свой класс очереди для обеспечения безопасности потоков.
  • Запускайте свои тесты, когда система находится в режиме ожидания, а также когда она находится под нагрузкой от других задач (например, наш сервер сборки часто запускает несколько сборок параллельно. много ошибок многопоточности, которые произошли, когда система находилась под нагрузкой.)
  • Избегайте утверждать о таймаутах. Если такое утверждение терпит неудачу, вы не знаете, нарушен ли код или слишком ли короткий тайм-аут. Вместо этого используйте очень щедрый тайм-аут (просто чтобы убедиться, что тест в конечном итоге не удался). Если вы хотите проверить, что операция не занимает больше времени, чем определенное время, измерьте продолжительность, но не используйте таймаут для этого.
+0

Первая техника, которую вы упомянули, очень хорошо работает для меня на практике. Я увеличиваю параллелизм в 20 - 100 раз и запускаю свои приложения. В сочетании с программированием «Конструирование по контракту» мне удается выкурить большинство проблем параллелизма. Я также делаю полный осмотр многопоточного кода, так как есть тонкие ошибки, которые появляются очень редко. – trshiv

1

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

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

1

Похоже, вы используете инструменты Microsoft. В Microsoft Research работает группа, которая работает над инструментом, специально разработанным для того, чтобы избавиться от параллелизма bugz. Выезд CHESS. Другими исследовательскими проектами на ранних этапах являются Cuzz и Featherlite.

VS2010 включает в себя очень хорошо выглядящий параллелизмом профайлер, видео available here.

Смежные вопросы