3.3.2. Защита редко обновляемых структур данных

Рассмотрим таблицу, в которой хранится кэш записей DNS, необходимых для установления соответствия между доменными именами и IP-адресами. Как правило, записи DNS остаются неизменными в течение длительного времени — зачастую многих лет. Новые записи, конечно, добавляются — скажем, когда открывается новый сайт — но на протяжении всей своей жизни обычно не меняются. Периодически необходимо проверять достоверность данных в кэше, но и тогда обновление требуется, лишь если данные действительно изменились.

Но хотя обновления происходят редко, они все же случаются, и если к кэшу возможен доступ со стороны нескольких потоков, то необходимо обеспечить надлежащую защиту, чтобы ни один поток, читающий кэш, не увидел наполовину обновленной структуры данных. Если структура данных не специализирована для такого способа использования (как описано в главах 6 и 7), то поток, который хочет обновить данные, должен получить монопольный доступ к структуре на все время выполнения операции. После того как операция обновления завершится, структуру данных снова смогут одновременно читать несколько потоков.

Использование std::mutex для защиты такой структуры данных излишне пессимистично, потому что при этом исключается даже возможность одновременного чтения, когда никакая модификация не производится. Нам необходим какой-то другой вид мьютекса. Такой мьютекс есть, и обычно его называют мьютексом чтения-записи (reader-writer mutex), потому что он допускает два режима: монопольный доступ со стороны одного «потока-писателя» и параллельный доступ со стороны нескольких «потоков-читателей».

В новой стандартной библиотеке С++ такой мьютекс не предусмотрен, хотя комитету и было подано предложение[6]. Поэтому в этом разделе мы будем пользоваться реализацией из библиотеки Boost, которая основана на отвергнутом предложении. В главе 8 вы увидите, что использование такого мьютекса — не панацея, а его производительность зависит от количества участвующих процессоров и относительного распределения нагрузки между читателями и писателями. Поэтому важно профилировать работу программу в целевой системе и убедиться, что добавочная сложность действительно дает какой-то выигрыш.

Итак, вместо std::mutex мы воспользуемся для синхронизации объектом boost::shared_mutex. При выполнении обновления мы будем использовать для захвата мьютекса шаблоны std::lock_guard<boost::shared_mutex> и std::unique_lock<boost::shared_mutex>, параметризованные классом boost::shared_mutex, а не std::mutex. Они точно так же гарантируют монопольный доступ. Те же потоки, которым не нужно обновлять структуру данных, могут воспользоваться классом boost::shared_lock<boost::shared_mutex> для получения разделяемого доступа. Применяется он так же, как std::unique_lock, но в семантике имеется одно важное отличие: несколько потоков могут одновременно получить разделяемую блокировку на один и тот же объект boost::shared_mutex. Однако если какой-то поток уже захватил разделяемую блокировку, то любой поток, который попытается захватить монопольную блокировку, будет приостановлен до тех пор, пока все прочие потоки не освободят свои блокировки. И наоборот, если какой-то поток владеет монопольной блокировкой, то никакой другой поток не сможет получить ни разделяемую, ни монопольную блокировку, пока первый поток не освободит свою.

В листинге ниже приведена реализация простого 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().