В следующем листинге воспроизведен код потокобезопасного стека из главы 3. Задача состояла в том, чтобы реализовать потокобезопасную структуру данных наподобие std::stack<>, которая поддерживала бы операции заталкивания и выталкивания.
Листинг 6.1. Определение класса потокобезопасного стека
#include
struct empty_stack: std::exception {
const char* what() const throw();
};
template
class threadsafe_stack {
private:
std::stack
mutable std::mutex m;
public:
threadsafe_stack(){}
threadsafe_stack(const threadsafe_stack& other) {
std::lock_guard
data = other.data;
}
threadsafe_stack& operator=(const threadsafe_stack&) = delete;
void push(T new_value) {
std::lock_guard
data.push(std::move(new_value)); ←(1)
}
std::shared_ptr
std::lock_guard
if (data.empty()) throw empty_stack(); ←(2)
std::shared_ptr
std::make_shared(3)
data.pop(); ←(4)
return res;
}
void pop(T& value) {
std::lock_guard
if (data.empty()) throw empty_stack();
value = std::move(data.top()); ←(5)
data.pop(); ←(6)
}
bool empty() const {
std::lock_guard
return data.empty();
}
};
Посмотрим, как в этом случае применяются сформулированные выше рекомендации. Во-первых, легко видеть, что базовую потокобезопасность обеспечивает защита каждой функции-члена с помощью мьютекса m. Он гарантирует, что в каждый момент времени к данным может обращаться только один поток, поэтому если функции-члены поддерживают какие-то инварианты, то ни один поток не увидит их нарушения.
Во-вторых, существует потенциальная гонка между empty() и любой из функций pop(), но поскольку мы явно проверяем, что стек пуст, удерживая блокировку в pop(), эта гонка не проблематична. Возвращая извлеченные данные прямо в pop(), мы избегаем потенциальной гонки, которая могла бы случиться, если бы top() и pop() были отдельными функциями-членами, как в std::stack<>.
Далее, существует несколько возможных источников исключений. Операция захвата мьютекса может возбудить исключение, но, во-первых, это крайне редкий случай (свидетельствующий о проблемах в реализации мьютекса или о нехватке системных ресурсов), а, во-вторых, эта операция всегда выполняется в самом начале любой функции-члена. Поскольку в этот момент никакие данные еще не изменены, опасности нет. Операция освобождения мьютекса не может завершиться ошибкой, она всегда безопасна, а использование std::lock_guard<> гарантирует, что мьютекс не останется захваченным.
Вызов data.push() (1) может возбудить исключение, если его возбуждает копирование или перемещение данных либо если памяти недостаточно для увеличения размера структуры, в которой хранятся сами данные. В любом случае std::stack<> гарантирует безопасность, поэтому здесь проблемы тоже нет.
В первом перегруженном варианте pop() наш код может возбудить исключение empty_stack (2), но в этот момент еще ничего не изменено, так что мы в безопасности. Создание объекта res (3) может возбудить исключение по двум причинам: при обращении к std::make_shared может не хватить памяти для нового объекта и внутренних данных, необходимых для подсчёта ссылок, или копирующий либо перемещающий конструктор возбуждает исключение при копировании или перемещении данных в только что выделенную область памяти. В обоих случаях исполняющая среда С++ и стандартная библиотека гарантируют отсутствие утечек памяти и корректное уничтожение нового объекта (если он был создан). Поскольку мы все еще не модифицировали данные стека, все хорошо. Вызов data.pop() (4) гарантированно не возбуждает исключений, равно как и возврат результата, так что этот вариант pop() безопасен относительно исключений.
Второй перегруженный вариант pop() аналогичен, только на этот раз исключение может возбудить оператор копирующего или перемещающего присваивания (5), а не конструктор нового объекта или экземпляра std::shared_ptr. Но и теперь мы ничего не изменяли до вызова функции data.pop() (6), которая гарантированно не возбуждает исключений, так что и этот вариант безопасен относительно исключений.