Параллельное программирование на С++ в действии. Практика разработки многопоточных программ | страница 46



и >size(), нельзя полагаться — хотя в момент вызова они, возможно, и были правильны, но после возврата из функции любой другой поток может обратиться к стеку и затолкнуть в него новые элементы, либо вытолкнуть существующие, причем это может произойти до того, как у потока, вызвавшего >empty() или >size(), появится шанс воспользоваться полученной информацией.

Если экземпляр >stackне является разделяемым, то нет ничего страшного в том, чтобы проверить, пуст ли стек с помощью >empty(), а затем, если стек не пуст, вызвать >top() для доступа к элементу на вершине стека:

>stack s;

>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() не будет гонки ни в одном потоке. Теперь рассмотрим возможные варианты выполнения программы.