В новой стандартной библиотеке С++ такой мьютекс не предусмотрен, хотя комитету и было подано предложение[6]. Поэтому в этом разделе мы будем пользоваться реализацией из библиотеки Boost, которая основана на отвергнутом предложении. В главе 8 вы увидите, что использование такого мьютекса — не панацея, а его производительность зависит от количества участвующих процессоров и относительного распределения нагрузки между читателями и писателями. Поэтому важно профилировать работу программу в целевой системе и убедиться, что добавочная сложность действительно дает какой-то выигрыш.
Итак, вместо std::mutex
мы воспользуемся для синхронизации объектом boost::shared_mutex
. При выполнении обновления мы будем использовать для захвата мьютекса шаблоны std::lock_guard
и std::unique_lock
, параметризованные классом boost::shared_mutex
, а не std::mutex
. Они точно так же гарантируют монопольный доступ. Те же потоки, которым не нужно обновлять структуру данных, могут воспользоваться классом boost::shared_lock
для получения std::unique_lock
, но в семантике имеется одно важное отличие: несколько потоков могут одновременно получить разделяемую блокировку на один и тот же объект boost::shared_mutex
. Однако если какой-то поток уже захватил разделяемую блокировку, то любой поток, который попытается захватить монопольную блокировку, будет приостановлен до тех пор, пока все прочие потоки не освободят свои блокировки. И наоборот, если какой-то поток владеет монопольной блокировкой, то никакой другой поток не сможет получить ни разделяемую, ни монопольную блокировку, пока первый поток не освободит свою.
В листинге ниже приведена реализация простого DNS-кэша, в котором данные хранятся в контейнере std::map
, защищенном с помощью boost::shared_mutex
.
Листинг 3.13. Защита структуры данных с помощью boost::shared_mutex
#include
#include
#include
#include
class dns_entry;
class dns_cache {
std::map
mutable boost::shared_mutex entry_mutex;
public:
dns_entry find_entry(std::string const& domain) const {
boost::shared_lock
(1)
std::map
entries.find(domain);
return (it == entries.end()) ? dns_entry() : it->second;
}
void update_or_add_entry(std::string const& domain,
dns_entry const& dns_details) {
std::lock_guard
(2)
entries[domain] = dns_details;
}
};
В листинге 3.13 в функции find_entry()
используется объект boost::shared_lock<>
, обеспечивающий разделяемый доступ к данным для чтения (1); следовательно, ее можно спокойно вызывать одновременно из нескольких потоков. С другой стороны, в функции update_or_add_entry()
используется объект std::lock_guard<>
, который обеспечивает монопольный доступ на время обновления таблицы (2), и, значит, блокируются не только другие потоки, пытающиеся одновременно выполнить update_or_add_entry()
, но также потоки, вызывающие find_entry()
.
3.3.3. Рекурсивная блокировка
Попытка захватить std::mutex
в потоке, который уже владеет им, является ошибкой и приводит к std::recursive_mutex
. Работает он аналогично std::mutex
, но с одним отличием: один и тот же поток может многократно захватывать данный мьютекс. Но перед тем как этот мьютекс сможет захватить другой поток, его нужно освободить столько раз, сколько он был захвачен. Таким образом, если функция lock()
вызывалась три раза, то и функцию unlock() нужно будет вызвать трижды. При правильном использовании std::lock_guard
и std::unique_lock
это гарантируется автоматически.