Некоторые из описанных средств тестирования следовало бы включать в стандартную библиотеку С++, а другие можно было бы реализовать на основе стандартной библиотеке как часть тестового стенда.
Обсудив различные способы исполнения тестового кода, посмотрим, как можно структурировать код для достижения желаемого порядка планирования потоков.
10.2.5. Структурирование многопоточного тестового кода
В разделе 10.2.2 я говорил о том, что нужно придумать, как обеспечить надлежащий порядок планирования для циклов «while» в тестах. Сейчас самое время поговорить о возникающих здесь вопросах.
Основная проблема — организовать набор потоков таким образом, чтобы каждый исполнял выбранный фрагмент кода в указанный вами момент времени. В простейшем случае потоков всего два, но решение легко обобщается и на большее число. На первом этапе нужно определиться, как устроен каждый тест:
• код общей настройки, исполняемый в самом начале;
• потоковый код настройки, исполняемый в каждом потоке;
• содержательный код, исполняемый в параллельно работающих потоках;
• код, исполняемый по завершении параллельного исполнения; может включать утверждения о состоянии программы.
Для определённости рассмотрим пример из списка в разделе 10.2.2: один поток вызывает push()
для пустой очереди, а второй в это время вызывает pop()
.
Код общей настройки очевиден: надо создать очередь. В потоке, исполняющем pop()
, нет потокового кода настройки. Потоковый код настройки для потока, исполняющего push()
, зависит от интерфейса очереди и типа сохраняемого в ней объекта. Если конструировать сохраняемый объект дорого или память для него должна выделяться из кучи, то лучше сделать это в потоковом коде настройки, чтобы не оказывать влияния на сам тест. С другой стороны, если в очереди хранятся всего лишь значения типа int
, то мы ничего не выиграем от их конструирования в коде настройки. Собственно тестируемый код тоже прост — вызвать push()
в одном потоке и pop()
в другом. А вот как быть с кодом, «исполняемым по завершении»?
В данном случае всё зависит от того, что должна делать функция pop()
. Если предполагается, что она блокирует поток до появления данных в очереди, то, очевидно, мы ожидаем, что будут возвращены данные, переданные функции push()
, и что очередь в итоге окажется пустой. Если же pop()
не блокирует поток и может вернуть управление, даже когда очередь пуста, то требуется проверить два возможных исхода: либо pop()
вернула данные, переданные push()
, и очередь пуста, либо pop()
известила об отсутствии данных и в очереди есть один элемент. Истинно должно быть ровно одно утверждение; чего мы точно не хотим, так это ситуации, когда pop()
говорит «нет данных», но очередь пуста, или когда pop()
вернула значение, а очередь все равно не пуста. Для упрощения теста предположим, что функция pop()
блокирующая. Тогда в завершающем коде должно быть утверждение вида «извлеченное значение совпадает с помещённым и очередь пуста».
Определившись со структурой кода, мы должны постараться, чтобы все работало в соответствии с планом. Один из путей - воспользоваться набором объектов std::promise
, обозначающих, что все готово. Каждый поток устанавливает обещание, сообщая, что он готов, а затем ждет (копии) будущего результата std::shared_future
, полученного из третьего объекта s td::promise
; главный поток ждет обещаний от всех потоков, а затем запускает потоки, устанавливая go
. Тем самым гарантируется, что каждый поток запущен и находится в точке, непосредственно предшествующей коду, который должен выполняться параллельно; весь потоковый код настройки должен завершиться до установки обещания go
. Наконец, главный поток ждет завершения других потоков и проверяет получившееся состояние. Мы также должны принять во внимание исключения и гарантировать, что ни один поток не будет ждать сигнала go
, который никогда не поступит. В листинге ниже приведён один из возможных способов структурирования этого теста.
Листинг 10.1.Пример теста, проверяющего параллельное выполнение функций очереди push()
и pop()
void test_concurrent_push_and_pop_on_empty_queue() {
threadsafe_queue q; ←
(1)
std::promise go, push_ready, pop_ready;←
(2)
std::shared_future
ready(go.get_future()); ←
(3)
std: :future push_done; ←
(4)
Читать дальше