Параллельное программирование на С++ в действии. Практика разработки многопоточных программ | страница 70
В листинге ниже приведена реализация простого 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
это гарантируется автоматически.
Как правило, программу, в которой возникает необходимость в рекурсивном мьютексе, лучше перепроектировать. Типичный пример использования рекурсивного мьютекса возникает, когда имеется класс, к которому могут обращаться несколько потоков, так что для защиты его данных необходим мьютекс. Каждая открытая функция-член захватывает мьютекс, что-то делает, а затем освобождает его. Но бывает, что одна открытая функция-член вызывает другую, и в таком случае вторая также попытается захватить мьютекс, что приведет к неопределенному поведению. Тогда, чтобы решить проблему по-быстрому, обычный мьютекс заменяют рекурсивным. Это позволит второй функции захватить мьютекс и продолжить работу.