3.2.2. Структурирование кода для защиты разделяемых данных

Как мы только что видели, для защиты данных с помощью мьютекса недостаточно просто «воткнуть» объект std::lock_guard в каждую функцию-член: один-единственный «отбившийся» указатель или ссылка сводит всю защиту на нет. На некотором уровне проверить наличие таких отбившихся указателей легко — если ни одна функция-член не передает вызывающей программе указатель или ссылку на защищенные данные в виде возвращаемого значения или выходного параметра, то данные в безопасности. Но стоит копнуть чуть глубже, как выясняется, что всё не так просто, — а просто никогда не бывает. Недостаточно проверить, что функции-члены не возвращают указатели и ссылки вызывающей программе, нужно еще убедиться, что такие указатели и ссылки не передаются в виде входных параметров вызываемым ими функциям, которые вы не контролируете. Это ничуть не менее опасно — что, если такая функция сохранит где-то указатель или ссылку, а потом какой-то другой код обратится к данным, не захватив предварительно мьютекс? Особенно следует остерегаться функций, которые передаются во время выполнения в виде аргументов или иными способами, как показано в листинге 3.2.

Листинг 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<typename Function>

 void process_data(Function func)  (1) Передаем

 {                                 │ "защищенные"

  std::lock_guard<std::mutex> l(m);│ данные поль-

  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).

Здесь фундаментальная проблема заключается в том, что мы не сделали того, что собирались сделать: пометить все участки кода, в которых имеется доступ к структуре данных, как взаимно исключающие. В данном случае мы забыли о коде внутри foo(), который вызывает unprotected->do_something(). К сожалению, в этом стандартная библиотека С++ нам помочь не в силах: именно программист должен позаботиться о том, чтобы защитить данные мьютексом. Но не всё так мрачно — следование приведенной ниже рекомендации выручит в таких ситуациях. Не передавайте указатели и ссылки на защищенные данные за пределы области видимости блокировки никаким способом, будь то возврат из функции, сохранение в видимой извне памяти или передача в виде аргумента пользовательской функции.

Хотя описанная только что ситуация — самая распространенная ошибка при защите разделяемых данных, перечень подводных камней ей отнюдь не исчерпывается. В следующем разделе мы увидим, что гонка возможна даже, если данные защищены мьютексом.