data_packet receive_data() { ←
(3)
std::call_once( │
connection_init_flag, &X::open_connection, 2)
(2)
this); ←┘
return connection.receive_data();
}
};
В этом примере инициализация производится либо при первом обращении к send_data()
(1), либо при первом обращении к receive_data()
(3). Поскольку данные инициализируются функцией-членом open_connection()
, то требуется передавать также указатель this
. Как и во всех функциях из стандартной библиотеки, которые принимают объекты, допускающие вызов, (например, конструктор std::thread
и функция std::bind()
), это делается путем передачи std::call_once()
дополнительного аргумента (2).
Следует отметить, что, как и в случае std:mutex
, объекты типа std::once_flag
нельзя ни копировать, ни перемещать, поэтому, если вы собираетесь использовать их как члены классы, то соответствующие конструкторы придется определить явно (если это необходимо).
Возможность гонки при инициализации возникает, в частности, при объявлении локальной переменной с классом памяти static
. По определению, инициализация такой переменной происходит, когда поток управления программы первый раз проходит через ее объявление. Но если функция вызывается в нескольких потоках, то появляется потенциальная возможность гонки за то, кто определит переменную первым. Во многих компиляторах, выпущенных до утверждения стандарта С++11, эта гонка действительно приводит к проблемам, потому что любой из нескольких потоков, полагая, что успел первым, может попытаться инициализировать переменную. Может также случиться, что некоторый поток попытается использовать переменную после того, как инициализация началась в другом потоке, но до того, как она закончилась. В С++11 эта проблема решена: по определению, инициализация производится ровно в одном потоке, и никакому другому потоку не разрешено продолжать выполнение, пока инициализация не завершится, поэтому потоки конкурируют лишь за право выполнить инициализацию первым, ничего более серьёзного случиться не может. Это свойство можно использовать как альтернативу функции std::call_once
, когда речь идет об инициализации единственной глобальной переменной:
class my_class;
my_class& get_my_class_instance() {
static my_class instance; ←┐
Гарантируется, что инициализация
return instance;
(1) потокобезопасна
}
Теперь несколько потоков могут вызывать функцию get_my_class_instance()
(1), не опасаясь гонки при инициализации.
Защита данных только на время инициализации — частный случай более общего сценария: доступ к редко обновляемой структуре данных. Обычно к такой структуре обращаются для чтения, когда ни о какой синхронизации можно не беспокоиться. Но иногда требуется обновить данные в ней. Нам необходим такой механизм защиты, который учитывал бы эти особенности.
3.3.2. Защита редко обновляемых структур данных
Рассмотрим таблицу, в которой хранится кэш записей DNS, необходимых для установления соответствия между доменными именами и IP-адресами. Как правило, записи DNS остаются неизменными в течение длительного времени — зачастую многих лет. Новые записи, конечно, добавляются — скажем, когда открывается новый сайт — но на протяжении всей своей жизни обычно не меняются. Периодически необходимо проверять достоверность данных в кэше, но и тогда обновление требуется, лишь если данные действительно изменились.
Но хотя обновления происходят редко, они все же случаются, и если к кэшу возможен доступ со стороны нескольких потоков, то необходимо обеспечить надлежащую защиту, чтобы ни один поток, читающий кэш, не увидел наполовину обновленной структуры данных. Если структура данных не специализирована для такого способа использования (как описано в главах 6 и 7), то поток, который хочет обновить данные, должен получить монопольный доступ к структуре на все время выполнения операции. После того как операция обновления завершится, структуру данных снова смогут одновременно читать несколько потоков.
Использование std::mutex
для защиты такой структуры данных излишне пессимистично, потому что при этом исключается даже возможность одновременного чтения, когда никакая модификация не производится. Нам необходим какой-то другой вид мьютекса. Такой мьютекс есть, и обычно его называют мьютексом