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



параметров вызываемым ими функциям, которые вы не контролируете. Это ничуть не менее опасно — что, если такая функция сохранит где-то указатель или ссылку, а потом какой-то другой код обратится к данным, не захватив предварительно мьютекс? Особенно следует остерегаться функций, которые передаются во время выполнения в виде аргументов или иными способами, как показано в листинге 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

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

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

>  std::lock_guard 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(). К сожалению, в этом стандартная библиотека С++ нам помочь не в силах: именно программист должен позаботиться о том, чтобы защитить данные мьютексом. Но не всё так мрачно — следование приведенной ниже рекомендации выручит в таких ситуациях. Не передавайте указатели и ссылки на защищенные данные за пределы области видимости блокировки никаким способом, будь то возврат из функции, сохранение в видимой извне памяти или передача в виде аргумента пользовательской функции.

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