Подход к тестированию многопоточного программного обеспечения - PullRequest
11 голосов
/ 13 марта 2010

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

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

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

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

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

Ответы [ 6 ]

10 голосов
/ 13 марта 2010

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

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

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

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

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

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

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

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

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

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

2 голосов
/ 13 марта 2010

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

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

2 голосов
/ 13 марта 2010

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

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

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

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

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

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

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

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

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

  • Поток A: считывание refcount из памяти для регистрации (refcount: 8)
  • Тема A: инкрементный регистр
    • ИЗМЕНЕНИЕ КОНТЕКСТА -
  • Поток B: считывание refcount из памяти для регистрации (refcount: 8)
  • Тема B: инкрементный регистр
  • Поток B: запись повторного счета из регистра в память (refcount: 9)
    • ИЗМЕНЕНИЕ КОНТЕКСТА -
  • Поток A: записать refcount из регистра в память (refcount: 9)

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

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

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

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

1 голос
/ 13 мая 2010

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

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 голос
/ 13 марта 2010

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

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

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