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