В данном случае оператор сравнения сначала получает сравниваемые значения, вызывая функцию-член get_detail()
(2), (3). Эта функция извлекает значение, находясь под защитой мьютекса (1). После этого оператор сравнивает полученные значения (4). Отметим, однако, что наряду с уменьшением времени удержания блокировки за счет того, что в каждый момент захвачен только один мьютекс (и, стало быть, исключена возможность взаимоблокировки), мы немного изменили семантику операции по сравнению с реализацией, в которой оба мьютекса захватываются вместе. Если оператор в листинге 3.10 возвращает true
, то это означает лишь, что значение lhs.some_detail
в один момент времени равно значению rhs.some_detail
в другой момент времени. Между двумя операциями считывания значения могли измениться как угодно; например, между точками (2) и (3) программа могла обменять их местами, и тогда сравнение оказалось бы вообще бессмысленным. Таким образом, возврат оператором сравнения значения true
, означает, что значения были равны, пусть даже ни в какой момент времени фактическое равенство не наблюдалось. Очень важно следить, чтобы такие изменения семантики операций не приводили к проблемам:
Иногда подходящего уровня гранулярности просто не существует, потому что не все операции доступа к структуре данных требуют одного и того же уровня защиты. В таком случае вместо простого класса std::mutex
стоит поискать альтернативный механизм.
3.3. Другие средства защиты разделяемых данных
Хотя мьютексы и представляют собой наиболее общий механизм, но они не единственные игроки на поле защиты разделяемых данных. Есть и другие механизмы, обеспечивающие защиту в специальных случаях.
Один такой крайний (но на удивление распространенный) случай возникает, когда разделяемые данные нуждаются в защите от одновременного доступа только на этапе инициализации, а потом уже никакой синхронизации не требуется. Так может быть, например, потому что после инициализации данные только читаются или потому что необходимая защита обеспечивается неявно как часть операций над данными. Как бы то ни было, захватывать мьютекс после того, как данные инициализированы, совершенно не нужно, это только снизило бы производительность. Поэтому в стандарте С++ предусмотрен механизм, служащий исключительно для защиты разделяемых данных во время инициализации.
3.3.1. Защита разделяемых данных во время инициализации
Предположим, имеется разделяемый ресурс, конструирование которого обходится настолько дорого, что мы хотим делать это, лишь когда действительно возникает необходимость; быть может, конструктор открывает базу данных или выделяет очень много памяти. Такая
std::shared_ptr
void foo() {
if (!resource_ptr) {
resource_ptr.reset(new some_resource); ←
(1)
}
resource_ptr->do_something();
}
Если сам разделяемый ресурс безопасен относительно одновременного доступа, то при переходе к многопоточной реализации единственная нуждающаяся в защите часть — инициализация (1), однако наивный подход, показанный в листинге ниже, может привести к ненужной сериализации использующих ресурс потоков. Дело в том, что каждый поток должен ждать освобождения мьютекса, чтобы проверить, был ли ресурс уже инициализирован.
Листинг 3.11. Потокобезопасная отложенная инициализация с помощью мьютекса
std::shared_ptr
std::mutex resource_mutex; ←┐
В этой точке все потоки
│
сериализуются
void foo() {
std::unique_lock
if (!resource_ptr) {
resource_ptr.reset(new some_resource); ←┐
в защите нуж-
} │
дается только
lk.unlock(); │
инициализация
resource_ptr->do_something();
}