Мьютексы — наиболее общий механизм защиты данных в С++, но панацеей они не являются; важно структурировать код так, чтобы защитить нужные данные (см. раздел 3.2.2), и избегать состояний гонки, внутренне присущих интерфейсам (см раздел 3.2.3). С мьютексами связаны и собственные проблемы, а именно: взаимоблокировки (deadlock) (см. раздел 3.2.4), а также защита слишком большого или слишком малого количества данных (см. раздел 3.2.8). Но начнем с простого.
3.2.1. Использование мьютексов в С++
В С++ для создания мьютекса следует сконструировать объект типа std::mutex
, для захвата мьютекса служит функция-член lock()
, а для освобождения — функция-член unlock()
. Однако вызывать эти функции напрямую не рекомендуется, потому что в этом случае необходимо помнить о вызове unlock()
на каждом пути выхода из функции, в том числе и вследствие исключений. Вместо этого в стандартной библиотеке имеется шаблон класса std::lock_guard
, который реализует идиому RAII — захватывает мьютекс в конструкторе и освобождает в деструкторе, — гарантируя тем самым, что захваченный мьютекс обязательно будет освобожден. В листинге 3.1 показано, как с помощью классов std::mutex
и std::lock_guard
защитить список, к которому могут обращаться несколько потоков. Оба класса определены в заголовке .
Листинг 3.1.Защита списка с помощью мьютекса
#include
#include
#include
std::list some_list; ←
(1)
std::mutex some_mutex; ←
(2)
void add_to_list(int new_value) {
std::lock_guard guard(some_mutex); ←
(3)
some_list.push_back(new_value);
}
bool list_contains(int value_to_find) {
std::lock_guard guard(some_mutex); ←
(4)
return
std::find(some_list.begin(), some_list.end(), value_to_find) !=
some_list.end();
}
В листинге 3.1 есть глобальный список (1), который защищен глобальным же объектом std::mutex
(2). Вызов std::lock_guard
в add_to_list()
(3)и list_contains()
(4)означает, что доступ к списку из этих двух функций является взаимно исключающим: list_contains()
никогда не увидит промежуточного результата модификации списка, выполняемой в add_to_list()
.
Хотя иногда такое использование глобальных переменных уместно, в большинстве случаев мьютекс и защищаемые им данные помещают в один класс, а не в глобальные переменные. Это не что иное, как стандартное применение правил объектно-ориентированного проектирования; помещая обе сущности в класс, вы четко даете понять, что они взаимосвязаны, а, кроме того, обеспечиваете инкапсулирование функциональности и ограничение доступа. В данном случае функции add_to_list
и list_contains
следует сделать функциями-членами класса, а мьютекс и защищаемые им данные — закрытыми переменными-членами класса. Так будет гораздо проще понять, какой код имеет доступ к этим данным и, следовательно, в каких участках программы необходимо захватывать мьютекс. Если все функции-члены класса захватывают мьютекс перед обращением к каким-то другим данным-членам и освобождают по завершении действий, то данные оказываются надежно защищены от любопытствующих.
Впрочем, это не совсем верно, проницательный читатель мог бы заметить, что если какая-нибудь функция-член возвращает указатель или ссылку на защищенные данные, то уже неважно, правильно функции-члены управляют мьютексом или нет, ибо вы проделали огромную брешь в защите. Любой код, имеющий доступ к этому указателю или ссылке, может прочитать (и, возможно, модифицировать) защищенные данные, не захватывая мьютекс. Таким образом, для защиты данных с помощью мьютекса требуется тщательно проектировать интерфейс, гарантировать, что перед любым доступном к защищенным данным производится захват мьютекса, и не оставлять черных ходов.
3.2.2. Структурирование кода для защиты разделяемых данных
Как мы только что видели, для защиты данных с помощью мьютекса недостаточно просто «воткнуть» объект std::lock_guard
в каждую функцию-член: один-единственный «отбившийся» указатель или ссылка сводит всю защиту на нет. На некотором уровне проверить наличие таких отбившихся указателей легко — если ни одна функция-член не передает вызывающей программе указатель или ссылку на защищенные данные в виде возвращаемого значения или выходного параметра, то данные в безопасности. Но стоит копнуть чуть глубже, как выясняется, что всё не так просто, — а просто никогда не бывает. Недостаточно проверить, что функции-члены не возвращают указатели и ссылки вызывающей программе, нужно еще убедиться, что такие указатели и ссылки не передаются в виде входных параметров вызываемым ими функциям, которые вы не контролируете. Это ничуть не менее опасно — что, если такая функция сохранит где-то указатель или ссылку, а потом какой-то другой код обратится к данным, не захватив предварительно мьютекс? Особенно следует остерегаться функций, которые передаются во время выполнения в виде аргументов или иными способами, как показано в листинге 3.2.
Читать дальше