Параллельное программирование на С++ в действии. Практика разработки многопоточных программ | страница 47
Если стек защищен внутренним мьютексом, то в каждый момент времени лишь один поток может исполнять любую функцию-член стека, поэтому обращения к функциям-членам строго чередуются, тогда как вызовы >do_something()
могут исполняться параллельно. Вот одна из возможных последовательностей выполнения:
Поток А>- -
Поток В
>if (!s.empty())
> if (!s.empty())
> int const value = s.top();
> int const value = s.top();
>s.pop();
>do_something(value); s.pop();
> do_something(value);
Как видите, если работают только эти два потока, то между двумя обращениями к >top()
никто не может модифицировать стек, так что оба потока увидят одно и то же значение. Однако беда в том, что между обращениями к>pop()
нет обращений к>top()
. Следовательно, одно из двух хранившихся в стеке значений никто даже не прочитает, оно будет просто отброшено, тогда как другое будет обработано дважды. Это еще одно состояние гонки, и куда более коварное, чем неопределенное поведение в случае гонки между >empty()
и >top()
, — на первый взгляд, ничего страшного не произошло, а последствия ошибки проявятся, скорее всего, далеко от места возникновения, хотя, конечно, всё зависит от того, что именно делает функция >do_something()
.
Для решения проблемы необходимо более радикальное изменение интерфейса — выполнение обеих операций >top()
и >pop()
под защитой одного мьютекса. Том Каргилл[4] указал, что такой объединенный вызов приводит к проблемам в случае, когда копирующий конструктор объектов в стеке может возбуждать исключения. С точки зрения безопасности относительно исключений, задачу достаточно полно решил Герб Саттер[5], однако возможность возникновения гонки вносит в нее новый аспект.
Для тех, кто незнаком с историей вопроса, рассмотрим класс >stack
. Вектор — это контейнер с динамически изменяемым размером, поэтому при копировании вектора библиотека должна выделить из кучи память. Если система сильно загружена или имеются жесткие ограничения на ресурсы, то операция выделения памяти может завершиться неудачно, и тогда копирующий конструктор вектора возбудит исключение >std::bad_alloc
. Вероятность такого развития событий особенно велика, если вектор содержит много элементов. Если бы функция >pop()
возвращала вытолкнутое из стека значение, а не только удаляла его из стека, то мы получили бы потенциальную проблему: вытолкнутое значение возвращается вызывающей программе только после модификации стека, но в процессе копирования возвращаемых данных может возникнуть исключение. Если такое случится, то только что вытолкнутые данные будут потеряны — из стека они удалены, но никуда не скопированы! Поэтому проектировщики интерфейса