Параллельное программирование на С++ в действии. Практика разработки многопоточных программ | страница 46
>size()
, нельзя полагаться — хотя в момент вызова они, возможно, и были правильны, но после возврата из функции любой другой поток может обратиться к стеку и затолкнуть в него новые элементы, либо вытолкнуть существующие, причем это может произойти до того, как у потока, вызвавшего >empty()
или >size()
, появится шанс воспользоваться полученной информацией.Если экземпляр >stack
не является разделяемым, то нет ничего страшного в том, чтобы проверить, пуст ли стек с помощью >empty()
, а затем, если стек не пуст, вызвать >top()
для доступа к элементу на вершине стека:
>stack
>if (!s.empty()) ←
(1)
>{
> int const value = s.top(); ←
(2)
> s.pop(); ←
(3)
> do_something(value);
>}
Такой подход в однопоточном коде не только безопасен, но и единственно возможен: вызов >top()
для пустого стека приводит к неопределенному поведению. Но если объект >stack
является разделяемым, то такая последовательность операций уже не безопасна, так как между вызовами >empty()
(1) и >top()
(2) другой поток мог вызвать >pop()
и удалить из стека последний элемент. Таким образом, мы имеем классическую гонку, и использование внутреннего мьютекса для защиты содержимого стека ее не предотвращает. Это следствие дизайна интерфейса.
И что же делать? Поскольку проблема коренится в дизайне интерфейса, то и решать ее надо путем изменения интерфейса. Но возникает вопроса — как его изменить? В простейшем случае мы могли бы просто декларировать, что >top()
возбуждает исключение, если в момент вызова в стеке нет ни одного элемента. Формально это решает проблему, но затрудняет программирование, поскольку теперь мы должны быть готовы к перехвату исключения, даже если вызов >empty()
вернул >false
. По сути дела, вызов >empty()
вообще оказывается ненужным.
Внимательно присмотревшись к показанному выше фрагменту, мы обнаружим еще одну потенциальную гонку, на этот раз между вызовами >top()
(2) и >pop()
(3). Представьте, что этот фрагмент исполняют два потока, ссылающиеся на один и тот же объект >s
типа >stack
. Ситуация вполне обычная: при использовании потока для повышения производительности часто бывает так, что несколько потоков исполняют один и тот же код для разных данных, и разделяемый объект >stack
идеально подходит для разбиения работы между потоками. Предположим, что первоначально в стеке находится два элемента, поэтому можно с уверенностью сказать, что между >empty()
и >top()
не будет гонки ни в одном потоке. Теперь рассмотрим возможные варианты выполнения программы.