Мьютексы — наиболее общий механизм защиты данных в С++, но панацеей они не являются; важно структурировать код так, чтобы защитить нужные данные (см. раздел 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
защитить список, к которому могут обращаться несколько потоков. Оба класса определены в заголовке <mutex>
.
Листинг 3.1. Защита списка с помощью мьютекса
#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list; ←
(1)
std::mutex some_mutex; ←
(2)
void add_to_list(int new_value) {
std::lock_guard<std::mutex> guard(some_mutex); ←
(3)
some_list.push_back(new_value);
}
bool list_contains(int value_to_find) {
std::lock_guard<std::mutex> 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<std::mutex>
в add_to_list()
(3) и list_contains()
(4) означает, что доступ к списку из этих двух функций является взаимно исключающим: list_contains()
никогда не увидит промежуточного результата модификации списка, выполняемой в add_to_list()
.
Хотя иногда такое использование глобальных переменных уместно, в большинстве случаев мьютекс и защищаемые им данные помещают в один класс, а не в глобальные переменные. Это не что иное, как стандартное применение правил объектно-ориентированного проектирования; помещая обе сущности в класс, вы четко даете понять, что они взаимосвязаны, а, кроме того, обеспечиваете инкапсулирование функциональности и ограничение доступа. В данном случае функции add_to_list
и list_contains
следует сделать функциями-членами класса, а мьютекс и защищаемые им данные — закрытыми переменными-членами класса. Так будет гораздо проще понять, какой код имеет доступ к этим данным и, следовательно, в каких участках программы необходимо захватывать мьютекс. Если все функции-члены класса захватывают мьютекс перед обращением к каким-то другим данным-членам и освобождают по завершении действий, то данные оказываются надежно защищены от любопытствующих.
Впрочем, это не совсем верно, проницательный читатель мог бы заметить, что если какая-нибудь функция-член возвращает указатель или ссылку на защищенные данные, то уже неважно, правильно функции-члены управляют мьютексом или нет, ибо вы проделали огромную брешь в защите. Любой код, имеющий доступ к этому указателю или ссылке, может прочитать (и, возможно, модифицировать) защищенные данные, не захватывая мьютекс. Таким образом, для защиты данных с помощью мьютекса требуется тщательно проектировать интерфейс, гарантировать, что перед любым доступном к защищенным данным производится захват мьютекса, и не оставлять черных ходов.