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