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 следует сделать функциями-членами класса, а мьютекс и защищаемые им данные — закрытыми переменными-членами класса. Так будет гораздо проще понять, какой код имеет доступ к этим данным и, следовательно, в каких участках программы необходимо захватывать мьютекс. Если все функции-члены класса захватывают мьютекс перед обращением к каким-то другим данным-членам и освобождают по завершении действий, то данные оказываются надежно защищены от любопытствующих.
Впрочем, это не совсем верно, проницательный читатель мог бы заметить, что если какая-нибудь функция-член возвращает указатель или ссылку на защищенные данные, то уже неважно, правильно функции-члены управляют мьютексом или нет, ибо вы проделали огромную брешь в защите. Любой код, имеющий доступ к этому указателю или ссылке, может прочитать (и, возможно, модифицировать) защищенные данные, не захватывая мьютекс. Таким образом, для защиты данных с помощью мьютекса требуется тщательно проектировать интерфейс, гарантировать, что перед любым доступном к защищенным данным производится захват мьютекса, и не оставлять черных ходов.