Хотя иногда такое использование глобальных переменных уместно, в большинстве случаев мьютекс и защищаемые им данные помещают в один класс, а не в глобальные переменные. Это не что иное, как стандартное применение правил объектно-ориентированного проектирования; помещая обе сущности в класс, вы четко даете понять, что они взаимосвязаны, а, кроме того, обеспечиваете инкапсулирование функциональности и ограничение доступа. В данном случае функции add_to_list
и list_contains
следует сделать функциями-членами класса, а мьютекс и защищаемые им данные — закрытыми переменными-членами класса. Так будет гораздо проще понять, какой код имеет доступ к этим данным и, следовательно, в каких участках программы необходимо захватывать мьютекс. Если все функции-члены класса захватывают мьютекс перед обращением к каким-то другим данным-членам и освобождают по завершении действий, то данные оказываются надежно защищены от любопытствующих.
Впрочем, это не
3.2.2. Структурирование кода для защиты разделяемых данных
Как мы только что видели, для защиты данных с помощью мьютекса недостаточно просто «воткнуть» объект std::lock_guard
в каждую функцию-член: один-единственный «отбившийся» указатель или ссылка сводит всю защиту на нет. На некотором уровне проверить наличие таких отбившихся указателей легко — если ни одна функция-член не передает вызывающей программе указатель или ссылку на защищенные данные в виде возвращаемого значения или выходного параметра, то данные в безопасности. Но стоит копнуть чуть глубже, как выясняется, что всё не так просто, — а просто никогда не бывает. Недостаточно проверить, что функции-члены не возвращают указатели и ссылки вызывающей программе, нужно еще убедиться, что такие указатели и ссылки не передаются в виде
Листинг 3.2. Непреднамеренная передача наружу ссылки на защищённые данные
class some_data {
int а;
std::string b;
public:
void do_something();
};
class data_wrapper {
private:
some_data data;
std::mutex m;
public :
template
void process_data(Function func)
(1) Передаем
{ │
"защищенные"
std::lock_guard
данные поль-
func(data); ←┘
зовательской
}
функции
};
some_data* unprotected;
void malicious_function(some_data& protected_data) {
unprotected = &protected_data;
}
data_wrapper x;
void foo
(2) Передаем
{ │
вредоносную
x.process_data(malicious_function); ←┘
функцию
unprotected->do_something(); ←
(3) Доступ к "защищенным"
}
данным в обход защиты
В этом примере функция-член process_data
выглядит вполне безобидно, доступ к данным охраняется объектом std::lock_guard
, однако наличие обращения к переданной пользователем функции func
(1) означает, что foo
может передать вредоносную функцию malicious_function
, чтобы обойти защиту (2), а затем вызвать do_something()
, не захватив предварительно мьютекс (3).