Выбрать главу

В листинге ниже приведена реализация простого DNS-кэша, в котором данные хранятся в контейнере std::map, защищенном с помощью boost::shared_mutex.

Листинг 3.13. Защита структуры данных с помощью boost::shared_mutex

#include <map>

#include <string>

#include <mutex>

#include <boost/thread/shared_mutex.hpp>

class dns_entry;

class dns_cache {

 std::map<std::string, dns_entry> entries;

 mutable boost::shared_mutex entry_mutex;

public:

 dns_entry find_entry(std::string const& domain) const {

  boost::shared_lock<boost::shared_mutex> lk(entry_mutex); ← (1)

  std::map<std::string, dns_entry>::const_iterator const it =

   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<boost::shared_mutex> lk(entry_mutex); ← (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::recursive_mutex> и std::unique_lock<std::recursive_mutex> это гарантируется автоматически.

Как правило, программу, в которой возникает необходимость в рекурсивном мьютексе, лучше перепроектировать. Типичный пример использования рекурсивного мьютекса возникает, когда имеется класс, к которому могут обращаться несколько потоков, так что для защиты его данных необходим мьютекс. Каждая открытая функция-член захватывает мьютекс, что-то делает, а затем освобождает его. Но бывает, что одна открытая функция-член вызывает другую, и в таком случае вторая также попытается захватить мьютекс, что приведет к неопределенному поведению. Тогда, чтобы решить проблему по-быстрому, обычный мьютекс заменяют рекурсивным. Это позволит второй функции захватить мьютекс и продолжить работу.

Однако такое решение не рекомендуется, потому что является признаком небрежного и плохо продуманного проектирования. В частности, при работе под защитой мьютекса часто нарушаются инварианты класса, а это означает, что вторая функция-член должна правильно работать даже в условиях, когда некоторые инварианты не выполняются. Обычно лучше завести новую закрытую функцию-член, которая вызывается из обеих открытых и не захватывает мьютекс (то есть предполагает, что мьютекс уже захвачен). Затем следует тщательно продумать, при каких условиях эта новая функция может вызываться и в каком состоянии будут при этом находиться данные.