Параллельное программирование на С++ в действии. Практика разработки многопоточных программ | страница 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() возвращала вытолкнутое из стека значение, а не только удаляла его из стека, то мы получили бы потенциальную проблему: вытолкнутое значение возвращается вызывающей программе только после модификации стека, но в процессе копирования возвращаемых данных может возникнуть исключение. Если такое случится, то только что вытолкнутые данные будут потеряны — из стека они удалены, но никуда не скопированы! Поэтому проектировщики интерфейса